sync.once 不是单例,仅保证初始化函数执行一次;单例对象需自行存储、管理与暴露,且必须声明为包级变量,初始化失败后无法重试或重置。

sync.Once 不是单例本身,它只保证「初始化函数执行一次」——你要的单例对象必须自己存、自己管、自己暴露。
为什么 sync.Once.Do 不能直接返回单例?
sync.Once.Do 的函数签名是 func(f func()),它只接受一个无参无返回值的函数。你没法靠它“拿到”实例,更没法把错误传出来。
- 常见错误写法:
once.Do(func() { return &Service{} })——return在闭包里毫无意义,编译都过不去 - 真正要做的,是把实例赋值给一个包级或结构体字段,比如
instance *Service - 别指望
Do帮你做空值检查或错误传播;失败了,instance就是nil,后续调用直接 panic
包级变量声明位置错了,sync.Once 就彻底失效
sync.Once 必须是包级变量(或嵌入全局结构体),否则每次调用都新建一个实例,完全失去“只一次”的语义。
Go语言(Golang)1.26.0版本提供 Go 官方 Windows amd64 MSI 安装包下载入口,版本号 1.26.0,可用于旧项目维护、兼容性测试和指定版本开发环境配置。
- 错:在函数里
once := sync.Once{}或结构体方法里var once sync.Once—— 每次都是新对象 - 对:统一声明在文件顶部,和实例变量放一起,比如
var instance *DB; var once sync.Once - 如果单例属于某个结构体(如
type CacheManager struct { db *sql.DB; once sync.Once }),那这个结构体本身也得是全局唯一的(比如也是包级变量)
初始化失败后,sync.Once 永远不会再试
一旦 Do 里的函数 panic 或中途出错,sync.Once 内部状态就标记为 done,后续所有调用直接跳过,instance 保持零值。
- 典型现象:
panic: runtime error: invalid memory address or nil pointer dereference,但堆栈里看不到初始化失败日志 - 必须显式存错:用
var initErr error或结构体字段err error,并在Do闭包里赋值 - 获取函数必须检查错误:
if initErr != nil { return nil, initErr },不能只判instance == nil - 初始化前加日志,失败后加校验,例如
if instance == nil { panic("DB init failed") }
别在 Do 里调用其他单例获取函数
这是死锁高发区。比如 GetDB() 的初始化函数里又调用了 GetCache(),而后者初始化又依赖 GetDB(),两个 sync.Once.Do 会互相等待,goroutine 永久阻塞。
-
sync.Once底层用的是不可中断、无超时的 futex 等待,卡住就是卡死 - 跨资源依赖必须拆解:要么提前按顺序显式初始化(如
InitDB()+InitCache()),要么把组合逻辑提到上层 - 测试时若需 mock,别硬编码
once.Do,应导出实例变量或用接口+依赖注入
sync.Once 标记为 done,就再也无法重置 —— 即使初始化失败,也没有 reset 方法,只能重启进程。golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










