sync.Once 不是单例模式实现,仅保证初始化逻辑最多执行一次;正确单例需结合包级变量、错误处理与非空保障,用结构体封装实例和error并返回(*T, error)。

sync.Once 不是单例模式的实现,它只是确保某段初始化逻辑最多执行一次。真正在 Go 里构建线程安全、可错误感知的单例,必须把它和包级变量、错误处理、非空保障一起用,缺一不可。
为什么直接赋值指针到 sync.Once.Do 是危险的
常见写法:instance = &MyService{} 放在 Do 里,看似完成了初始化,但一旦构造函数失败(比如依赖的配置缺失、DB 连接超时),instance 就是 nil,后续调用直接 panic 或静默返回空指针。
-
sync.Once.Do不捕获 panic,也不返回 error —— 它只管“是否已调用”,不管“调用是否成功” - 所有对
instance的读取都发生在Do返回之后,此时instance可能仍是零值 - 调用方无法区分“还没初始化”和“初始化失败”,错误被吞掉
正确姿势:用结构体封装实例 + 错误,且暴露 (*T, error)
必须把初始化结果(成功实例或失败 error)统一存进一个私有结构体,并让获取函数返回 (*T, error),而不是裸指针。
- 声明包级变量:
var serviceOnce sync.Once和var serviceInst struct{ instance *Service; err error } -
Do内部必须完整执行初始化并赋值:serviceInst = struct{...}{instance: s, err: err} - 对外函数签名必须是
func GetService() (*Service, error),不能省略 error - 避免把
instance和err拆成两个独立包级变量,否则类型不一致或并发读写可能出错
sync.Once 被重复初始化的典型场景
它只对「同一个实例」保证一次,不是对「同名逻辑」。以下写法会让 Do 每次都执行:
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
立即学习“go语言免费学习笔记(深入)”;
- 在函数内部声明:
func handler() { var once sync.Once; once.Do(...) }→ 每次调用都新建实例 - 作为 struct 值字段:
type S struct { once sync.Once }→ 每次new(S)都得到全新状态 - 测试中反复复用同一全局
sync.Once→ 第一个 test case 执行后,后续全部跳过,mock 失效
正确做法:包级变量声明;测试时改用局部 sync.Once 或抽离初始化逻辑为可注入函数。
别让首次请求卡在 Do 里等耗时操作
sync.Once.Do 是同步阻塞的。如果初始化要连远程配置中心、加载大文件或启动子进程,所有首个调用它的 goroutine 都会卡住,HTTP handler 就可能超时。
- 不要等到第一个 API 请求才触发初始化 —— 在
main()开头就主动调用一次获取函数 - 避免在
Do里做任何可能长时间阻塞的操作(如无 timeout 的 HTTP 请求、未设 deadline 的 DB dial) - 如果初始化逻辑本身需要重试或超时控制,
sync.Once不适合,应换用singleflight.Group或自定义懒加载封装
真正容易被忽略的是:一旦 Do 里的函数 panic,sync.Once 的状态就永久标记为 done,不会再尝试;但 panic 不会被 recover,也不会传给调用方 —— 你得自己在闭包里加 defer/recover 并把错误存到共享变量里,否则调用方永远不知道失败了。

















