sync.once.do不保证跨变量执行顺序,多个实例完全独立;其语义是“只执行一次”而非“成功一次”,panic后不再重试;必须作为包级变量使用,且有依赖的初始化需合并到同一do调用中。

sync.Once.Do 不保证跨变量执行顺序
多个 sync.Once 实例之间完全独立,没有隐式依赖或执行时序约束。比如你写了 dbOnce.Do(initDB) 和 cacheOnce.Do(initCache),它们谁先谁后、是否并发执行,全看哪个 goroutine 先抢到 CAS 成功——Go 不保证也不干预。
常见错误是以为“先调用 GetDB() 再调用 GetCache()”就能让 DB 初始化完再初始化 Cache。实际可能 Cache 先完成,DB 还卡在连接超时里;更糟的是,如果 initCache 依赖 initDB 的结果(比如要用 db 对象建索引),就会 panic 或 nil pointer dereference。
- 必须把有依赖关系的初始化逻辑合并进同一个
Do调用里,例如:once.Do(func() { initDB(); initCache() }) - 若组件间耦合太深,建议用一个初始化函数统一协调,而不是靠多个 Once 拆解
- 避免在
initDB里直接调GetCache(),否则可能触发循环依赖死锁(A 等 B,B 又等 A)
别把 sync.Once 当成“带重试的懒加载”
sync.Once.Do 的语义是“只执行一次”,不是“成功一次”。只要传入函数开始执行,无论正常返回还是 panic,内部 done 标志都会被设为 1,后续调用直接跳过——它不捕获 panic,也不重置状态。
典型踩坑:在 Do 里直接调 sql.Open 或 http.Get,网络抖动导致失败,panic 后整个单例永久不可用,且无日志、无提示。
- 必须在闭包内手动
defer func() { recover() }(),或把初始化逻辑抽成返回error的函数,在Do中判断并记录错误 - 不要指望 Once 自动重试;重试逻辑得自己写在外层,比如封装成
MustGetDB(retryTimes int) - 错误状态需要显式暴露,例如声明包级变量
var dbErr error,在Do中赋值,供调用方检查
sync.Once 和 init 函数不是替代关系,而是分工关系
init 是包加载时强制执行,不可控、不可延迟、不能传参;sync.Once 是运行时按需触发,支持环境判断、配置读取、I/O 等耗时操作。两者解决的问题根本不同。
Go语言(Golang)1.26.0版本提供 Go 官方 Windows amd64 MSI 安装包下载入口,版本号 1.26.0,可用于旧项目维护、兼容性测试和指定版本开发环境配置。
误用场景:把数据库连接写在 init 里,结果测试环境没配 DSN 就 panic;或者把 logger 初始化放 init,却想根据命令行参数选日志级别——做不到。
- 适合
init的:纯内存结构预热、常量校验、unsafe 操作注册 - 适合
sync.Once的:读配置文件、连 Redis、加载证书、启动后台 goroutine - 大型项目中,
init应极简,复杂初始化一律交给 Once 控制入口
包级变量声明位置决定 Once 是否生效
sync.Once 必须是包级变量,否则每次调用都新建一个实例,彻底失去“只执行一次”的意义。这是新手最常犯的硬伤。
危险写法:func GetService() *Service { var once sync.Once; once.Do(...) } —— 每次都 new 一个 Once,等于没用。
- 正确做法:在文件顶部用
var声明,和单例指针变量同级 - 结构体字段里嵌
sync.Once可行,但方法接收者必须是*T,否则复制的是副本 - 切记:Once 本身不导出(小写开头),只通过函数暴露访问入口
初始化顺序和错误传播都依赖这个变量的生命周期——它得活在整个程序运行期,不能被 GC 或作用域截断。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










