sync.Once 是 Go 单例初始化唯一靠谱的线程安全方案,但必须声明为包级变量、闭包内完成全部初始化链路(含 I/O 和错误赋值)、通过额外变量暴露错误,禁用 init() 或函数内新建实例。

直接说结论:用 sync.Once 是 Go 单例初始化唯一靠谱的线程安全方案,但只声明 once.Do 不等于写对了单例 —— 常见错误是变量作用域错、闭包 panic 后静默失败、或把 I/O 操作漏在 Do 外面。
为什么不能把 sync.Once 声明在函数里
每次调用函数都新建一个 sync.Once 实例,done 字段永远是 0,所有 goroutine 都会各自执行闭包,彻底失去“只一次”的意义。
- ❌ 错误写法:
func GetDB() *sql.DB { once := sync.Once{}; once.Do(initDB) } - ✅ 正确做法:声明为包级变量,和
instance、initErr同级 - 结构体字段嵌入时,方法接收者必须是指针(
func (c *Client) GetConn()),否则c.once是副本,不共享状态
sync.Once.Do 闭包里该塞什么
必须包裹整个初始化链路,不是只包 &Config{} 或 newService()。任何 I/O、解析、校验、连接测试,都得在里面做。
- os.ReadFile、viper.Unmarshal、
db.Ping()全部放进闭包 - 不要在外面调
c.LoadFromFile(),否则并发下重复读文件、重复建连接池 - 失败时必须显式赋值
initErr = err,不能只log.Fatal或panic—— 后者会让done变成 1,但instance还是零值,调用方拿nil直接 panic
怎么暴露初始化错误
sync.Once.Do 的函数签名是 func(f func()),不支持返回值。想让上层能判断失败,必须靠额外变量协同记录状态。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
立即学习“go语言免费学习笔记(深入)”;
- 声明两个包级变量:
var instance *Service和var initErr error - 闭包内统一赋值:
instance, initErr = newService(), err - 对外函数签名必须是
func GetService() (*Service, error),调用方必须检查err != nil - 别在
GetService()里重试 ——sync.Once不可逆,done设为 1 后永远跳过
为什么不能在 init() 里初始化单例
init() 在包导入时强制执行,无法延迟、无法传参、无法返回错误。一旦失败,整个程序启动崩溃,且堆栈指向模糊。
-
flag.Parse()还没跑,os.Getenv("DB_URL")可能为空 - 单元测试中环境变量被临时覆盖,
init()却已固化错误值 - 依赖其他包的
init()?顺序不可控,容易拿到nil依赖 - 适用场景仅限无副作用的小操作:注册
json.Encoder、预设常量 map
真正麻烦的从来不是写对 sync.Once,而是初始化逻辑本身是否可重试、是否需超时、是否允许部分失败 —— 这些它一概不管,得你手动兜底。

















