sync.once.do只执行一次,因其内部用atomic.loaduint32检查done字段(0未执行/1已执行),仅首次cas成功者加锁执行闭包并原子置1,后续调用直接返回;panic后done仍为1,不重试、不可逆。

sync.Once.Do 为什么只执行一次
因为 sync.Once 内部用 atomic.LoadUint32 原子读取 done 字段,值为 0 才进 doSlow;进 doSlow 后先加锁、再原子写入 1、最后执行闭包——一旦写成 1,后续所有调用直接跳过。这不是“靠运气”,而是 Go 运行时保证的 happens-before 语义:初始化函数里的所有写操作,对后续任何 goroutine 的读都可见。
常见错误现象:instance 有时是 nil、有时能用,日志里初始化逻辑被打印多次——说明没用 sync.Once,或用了但变量作用域错了。
-
sync.Once不是魔法,它只管“执行一次”,不管执行结果是否成功 - 如果闭包里
panic,done仍会被设为 1,后续调用直接跳过,instance保持零值 - 不能把
sync.Once声明在函数里,否则每次调用都新建一个,完全失效
带错误返回的单例初始化怎么写
sync.Once.Do 签名是 func(f func()),不支持返回值。想暴露错误,必须把 error 和实例一起存为包级变量,靠外部函数统一返回。
使用场景:数据库连接、配置加载、HTTP 客户端初始化——这些操作可能失败,且上层需要决策重试或降级。
- 声明两个包级变量:
instance *Service和initErr error - 在
once.Do闭包里调用初始化函数(如newService()),赋值两者 -
GetInstance()函数同时返回*Service和error,调用方必须检查 - 别在闭包里直接
log.Fatal或panic,那会掩盖错误、阻断恢复路径
为什么不能在 init() 里做单例初始化
因为 init() 在包导入时强制执行,无法延迟、无法传参、无法返回错误。一旦出错,整个程序启动失败,且堆栈指向模糊,难定位。
典型误用:init() 里调用 sql.Open、读取 os.Getenv、解析 flag —— 这些值在 init() 阶段往往还没准备好。
-
flag.Parse()在main()开头才执行,init()拿不到命令行参数 - 环境变量可能被测试代码临时覆盖,
init()却已固化了错误值 - 单元测试无法 mock 或重置,每个测试用例都得重启进程
- 依赖其他包的
init()结果?顺序不可控,容易出现空指针
结构体字段嵌入 sync.Once 的注意事项
当单例逻辑属于某个结构体(比如带缓存的 API 客户端),把 sync.Once 作为字段嵌入比全放包级更清晰,但必须注意接收者是指针。
容易踩的坑:func (s Service) Init() 是值接收者,每次调用都是副本,s.once 永远是新零值,根本起不到保护作用。
- 必须写成
func (s *Service) Init(),确保修改的是原结构体字段 - 结构体内嵌
sync.Once后,Init()方法要负责设置自身字段(如s.client),而非包级变量 - 多个方法共用同一个
sync.Once字段即可,不用每个方法配一个 - 测试时可直接 new 一个
*Service,调用Init(),不污染全局状态
sync.Once 不帮你做这件事,它只沉默地记下“我试过了”。golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











