sync.once.do不会重复执行,因其用uint32标志位done配合原子操作控制状态,函数一旦返回(含panic)即置done为1,后续调用直接返回;仅接受func()类型,需用闭包封装参数,且实例必须全局或长期持有。

sync.Once.Do 为什么不会重复执行?
因为 sync.Once 内部用一个 uint32 标志位(done)配合原子操作控制状态,一旦 Do 里的函数返回,标志位就被设为 1,后续所有调用都会直接返回,不进函数体。它不关心函数是否 panic——只要函数返回(无论正常还是 panic),都算“执行完成”,之后不再调用。
传给 sync.Once.Do 的函数必须是无参无返回值的 func()
sync.Once.Do 只接受 func() 类型,不能直接传带参数的初始化函数。常见错误是试图写成 once.Do(initDB("prod")),这会导致函数立即执行,且结果被忽略;正确做法是用闭包封装:
dbOnce := &sync.Once{}
var db *sql.DB
dbOnce.Do(func() {
var err error
db, err = sql.Open("mysql", "user:pass@tcp(127.0.0.1:3306)/test")
if err != nil {
log.Fatal(err) // 或按需处理
}
})
- 闭包里可安全捕获外部变量(如配置字符串、上下文等)
- 不要在闭包里做耗时阻塞操作以外的副作用,比如修改全局状态前没加锁,可能引发竞态
- 如果初始化逻辑需要返回值或错误,得靠闭包外的变量承接,
Do本身不传回任何东西
多个 sync.Once 实例不能共享“只执行一次”的语义
每个 sync.Once 实例独立维护自己的 done 标志。写成局部变量或每次 new 一个,就完全失去单例效果:
func badInit() {
once := &sync.Once{} // 每次调用都是新实例
once.Do(func() { log.Println("this runs every time") })
}
- 必须把
sync.Once实例定义为包级变量、结构体字段,或通过依赖注入长期持有 - 常见误用:在 HTTP handler 里声明
once := sync.Once{},导致每次请求都重初始化 - 如果初始化逻辑依赖请求参数(比如按 tenant ID 初始化不同 DB),
sync.Once就不适用,得换 map + sync.RWMutex 或其他方案
panic 会阻止后续调用,但不会回滚已发生的副作用
如果 Do 里的函数 panic,sync.Once 仍会标记为 done,后续调用直接返回,不会重试。但 panic 前已完成的操作(比如文件打开、内存分配、日志打印)不会自动撤销:
- 初始化函数应尽量幂等,或在 panic 前确保资源未泄露(如打开文件后 panic,要 defer close)
- 不要指望
sync.Once提供事务语义——它只管“执行一次”,不管“执行成功” - 若需失败后重试,得自己实现重试逻辑,
sync.Once不提供 retry 接口
sync.Once 实例生命周期。写完记得检查:那个 once 变量是不是真的只有一份,以及闭包里读的配置是不是初始化时的最新值。











