sync.Once必须声明为包级变量或结构体指针字段,局部声明会因每次新建实例导致done始终为0而失效;初始化失败需用额外变量记录error并显式返回;init()不可用于依赖外部资源的初始化。

必须把 sync.Once 声明为包级变量或结构体指针字段,否则它根本不起作用。
为什么局部声明的 sync.Once 会失效
每次函数调用都新建一个 sync.Once 实例,它的内部 done 字段永远是 0,所有 goroutine 都会各自执行闭包——完全失去“只执行一次”的语义。
- ❌ 错误写法:
func GetDB() *sql.DB { once := sync.Once{}; once.Do(...) } - ✅ 正确做法:把
once和instance一起声明为包级变量,作用域一致 - 结构体嵌入时,方法必须用指针接收者:
func (c *Client) GetConn();值接收者会操作副本,c.once不共享
初始化失败后怎么暴露 error
sync.Once.Do 的函数签名是 func(f func()),不支持返回值,也不捕获 panic。一旦初始化失败(比如 DB 连接超时),instance 保持零值,后续调用静默返回 nil,极易引发下游 panic。
- 用两个包级变量同步记录状态:
var serviceInst struct{ instance *Service; err error } -
once.Do闭包内调用带error的构造函数,并完整赋值:serviceInst = struct{...}{instance: s, err: err} - 对外函数必须返回
(*Service, error),调用方必须检查err != nil
能不能在 init() 里用 sync.Once
不能。init() 在包导入时强制执行,无法延迟、无法传参、无法返回 error。如果这时去连数据库或读配置文件,失败会导致整个程序启动崩溃,堆栈指向模糊,难定位。
立即学习“go语言免费学习笔记(深入)”;
-
init()只适合无副作用、确定成功的小操作:注册 encoder(如json.Register)、预设常量映射表 - 依赖外部资源(文件、网络、环境变量)的初始化,必须改用
sync.Once+ 显式GetXXX()函数 - 多个
init()执行顺序由导入路径决定,不可控;别指望靠它做依赖排序
多个资源初始化要不要共用一个 sync.Once
一个 sync.Once 只管一件事。想让“加载配置 + 初始化日志 + 建立 DB 连接”都只做一次,不能塞进同一个 Do 里再加 if 分支——那会破坏 once 语义,还可能漏执行。
- 要么用多个
sync.Once实例分别保护不同资源 - 要么把整套初始化逻辑打包成一个函数,由单个
sync.Once触发 - 不要在
Do闭包里调用其他单例的GetXXX(),否则容易循环依赖死锁
真正麻烦的不是写对 sync.Once,而是初始化逻辑本身是否可重试、是否需超时、是否允许部分失败——这些它一概不管,得靠你手动兜底。


















