sync.Once.Do 能保证只执行一次是因为内部用 uint32 原子变量配合 atomic.CompareAndSwapUint32 实现无锁判断,函数返回(含 panic)后状态永久置为“已完成”,后续调用直接跳过。
sync.Once.Do 为什么能保证只执行一次
因为
内部用一个 uint32 原子变量标记是否已执行,配合
实现无锁判断。只要
的函数返回,状态就永久置为“已完成”,后续所有调用直接跳过函数体——哪怕那个函数 panic 了,once 也认为“已经试过了”,不会再重试。
常见错误现象:
传入的函数里有 panic,结果单例没初始化成功,但后续调用全静默失败;或者误以为 once 能重试,手动包了一层 recover 却忘了同步返回值,导致变量仍是零值。
必须把初始化逻辑(包括赋值、连接、加载配置等)全部写在
的函数里,不能拆到外面
不要在
函数里做耗时阻塞操作(比如等网络超时),否则所有并发 goroutine 都会卡住等它结束
如果初始化可能失败,应在函数内处理错误并设默认值或 panic,
不负责错误传播
单例变量该声明成指针还是值类型
取决于初始化成本和是否需要方法集继承。绝大多数情况应该声明为
指针类型,原因很实际:避免每次调用都拷贝结构体;方便后续给类型加 receiver 方法;且
初始化后通常要长期持有并复用。
使用场景:数据库连接池、配置管理器、日志实例、全局缓存——这些对象本身体积大、带锁、有状态,天然适合指针。
立即学习
“
go语言免费学习笔记(深入)
”;
如果 T 是小结构体(比如只有几个 int 字段),且所有方法都是值接收者,用值类型也行,但可读性和扩展性差
千万别声明成
值类型再在
里取地址赋给全局变量,这会造成逃逸和额外分配
初始化函数返回值类型必须和变量类型严格一致,
就得让
里的函数赋值给
,不能返回
再取地址
多个单例之间有依赖时怎么安全初始化
本身不支持依赖声明,靠调用顺序和嵌套
实现。核心原则是:谁被依赖,谁先定义
和变量;依赖方在自己的
函数里主动触发被依赖方的
。
go语言参考手册 中文CHM版
Go 是一个开源的编程语言,它能让构造简单、可靠且高效的软件变得容易。本文给大家带来Go参考手册,需要的可以来下载! Go是从2007年末由Robert Griesemer, Rob Pike, Ken Thompson主持开发,后来还加入了Ian Lance Taylor, Russ Cox等人,并最终于2009年11月开源,在2012年早些时候发布了Go 1稳定版本。现在Go的开发已经是完全开放的,并且拥有一个活跃的社区。 Go 语言特色 简洁、快速、安全 并行、有趣、开源 内存管理、v数组安全、编译
下载
错误示例:A 依赖 B,但 A 的初始化函数里没调用
,而是直接访问
—— 此时 B 可能还没初始化,得到零值。
被依赖的单例(如 B)仍需独立的
和变量,不能省略
依赖方(如 A)的
函数里第一件事就是确保 B 已就绪:
避免循环依赖,否则会死锁;Go 编译期不检查,只能靠设计约束
如果依赖链深(A→B→C),每个环节都要显式触发前序
,别指望“全局 init 顺序”
为什么不用 init 函数替代 sync.Once
函数在包加载时执行,无法按需延迟初始化,也无法响应运行时参数(比如从 flag 或环境变量读配置)。更重要的是,
是包级单次,而
是变量级单次——你可以有多个
实例控制不同对象,但整个包只有一个
。
性能影响:init 在程序启动时集中执行,可能拖慢冷启动;
把开销摊到第一次使用时,更适合微服务或 CLI 工具这类按需加载场景。
如果单例初始化完全不依赖运行时输入(比如硬编码的常量映射表),
更简单,但灵活性归零
有轻微原子操作开销,但实测在大多数业务场景下可忽略(纳秒级)
测试时,
支持重置(通过反射或重新声明变量),
完全不可测
真正容易被忽略的是:一旦用
,就必须接受“初始化失败即永久失败”这个事实。没有 retry、没有 fallback、不抛异常给调用方——你得在
函数里自己兜底,比如连不上 Redis 就用内存 map 顶着,而不是指望下次调用能重来。
sync.Onceatomic.CompareAndSwapUint32DoDoDoDosync.Once*Tsync.OnceTDovar ins *ConfigDoinsConfigsync.OnceDoonceDoDoBOnce.Do(...)BInstancesync.OnceDoBOnce.Do(initB)Doinitinitsync.Oncesync.Onceinitsync.Onceinitsync.Oncesync.Onceinitsync.OnceDo