Golang如何使用sync.Once确保单次执行

来源:PHP编程网作者:新加坡程序员头衔:程序员
导读:本期聚焦于小伙伴创作的《Golang如何使用sync.Once确保单次执行》,敬请观看详情。在并发程序里重复初始化配置或连接池常引发数据竞争与资源浪费。sync.Once通过内部状态位与原子操作,保证某函数在多协程环境下仅运行一次。其Do方法接收无参函数,首次调用触发执行并加锁同步,后续调用直接跳过。相比自行用互斥锁加布尔变量判断,它避免了忘记加锁或判断顺序错误导致的多次执行问题。理解其底层done字段的原子读与慢路径加锁逻辑,能帮助开发者在初始化、懒加载场景中写出更安全的Go代码。

在Go语言并发编程中,某些初始化逻辑只想被执行一次,例如全局配置加载、数据库连接池创建或者单例对象构建。标准库中的sync.Once正是为解决这类需求而设计,它能在多个goroutine同时调用时,确保传入的函数只运行一次,且其他调用会阻塞至该次执行完成。

Golang如何使用sync.Once确保单次执行

sync.Once的基本用法

sync.Once是一个结构体,位于sync包中,本身零值即可用。其核心方法是Do(f func()),参数是一个无返回值的函数。无论多少个goroutine调用同一个Once实例的Do,函数f都只会被执行一次。这种机制非常适合延迟初始化,也就是平时说的懒加载。

下面是一段最基础的示例,模拟并发场景下只初始化一次计数器:

package main

import (
    "fmt"
    "sync"
    "time"
)

func main() {
    var once sync.Once
    var count int

    initFunc := func() {
        count++
        fmt.Println("初始化执行了")
    }

    for i := 0; i < 5; i++ {
        go func(idx int) {
            once.Do(initFunc)
            fmt.Printf("goroutine %d 完毕,count=%dn", idx, count)
        }(i)
    }

    time.Sleep(time.Second)
}

运行以上代码,控制台只会打印一次“初始化执行了”,而五个goroutine都读到了count等于1的结果。这说明once.Do内部已经帮我们处理好竞态,不需要自己写锁。

如果错误地使用多个Once实例,则无法保证单次执行。每个Once对象独立记录自己的执行状态,因此单例通常定义为包级变量,而不是在函数内局部声明。

底层实现原理

很多开发者好奇,sync.Once为什么这么可靠。其实它的源码非常精简,主要依赖一个done的uint32字段和一个互斥锁。首次进入Do时,会用atomic.LoadUint32读取done,如果已是1就直接返回;如果是0,则进入慢路径加锁,再次确认done为0后才执行函数,并将done置为1。

这种“双重检查”设计减少了锁开销:绝大多数后续调用只需一次原子读就能退出,不必抢锁。源码中slow-path的写法保证了即使多个goroutine同时穿过第一次检查,也只有一个能拿到锁并执行,其余在锁释放后看到done为1随即返回。

// 简化版逻辑,非标准库原文
type Once struct {
    done uint32
    m    sync.Mutex
}

func (o *Once) Do(f func()) {
    if atomic.LoadUint32(&o.done) == 0 {
        o.doSlow(f)
    }
}

func (o *Once) doSlow(f func()) {
    o.m.Lock()
    defer o.m.Unlock()
    if o.done == 0 {
        defer atomic.StoreUint32(&o.done, 1)
        f()
    }
}

需要注意,如果f函数在执行中发生panic,done依然会被置为1,Once不会再尝试执行第二次。这在一些必须成功初始化的场景里是隐患,调用方应在f内部自行recover或保证不panic。

另外,Once不支持重置。一旦执行过,该实例永久生效。若业务需要可重复触发初始化,应自己结合锁与版本号实现,而非复用Once。

常见使用场景与错误写法

最典型的应用是单例模式。下面展示一个线程安全的单例获取函数,利用包级Once避免重复构建:

package singleton

import "sync"

type Config struct {
    Name string
}

var (
    instance *Config
    once     sync.Once
)

func GetConfig() *Config {
    once.Do(func() {
        instance = &Config{Name: "default"}
    })
    return instance
}

这种方式比用map加锁缓存更直观,也不会出现判断nil后未加锁导致重复赋值的漏洞。不过要小心,Do的参数函数不应该调用同一个Once的Do,否则会死锁,因为slow-path持锁时又去获取同一把锁。

还有一种错误是以为Once能保证函数内所有副作用对外部可见的顺序。实际上,Once只保证函数执行完才让其他调用返回,函数内的变量发布仍应遵循Go内存模型,例如通过通道或原子变量同步。

与自行实现方案的对比

如果不使用sync.Once,开发者可能会写出如下代码:

var initialized bool
var mu sync.Mutex

func initConfig() {
    mu.Lock()
    defer mu.Unlock()
    if !initialized {
        // 做初始化
        initialized = true
    }
}

这种写法虽然也能工作,但每次调用都要加锁,性能较差;若改成先判断再加锁,又容易在极端并发下漏掉检查。sync.Once把这些细节封装好,既安全又高效。

从架构角度看,把“只做一次”的语义交给标准库,能让业务代码更聚焦于逻辑本身。在微服务启动、插件加载、全局监控注册等场合,合理使用sync.Once可以显著降低并发bug出现的概率。

sync.Once单例模式并发控制修改时间:2026-08-03 22:03:41

免责声明:​ 已尽一切努力确保本网站所含信息的准确性。网站内容多为原创整理与精心编撰,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们处理。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。