跳转到主内容
趣航编程网 - 趣学编程,启航技术之路!

使用Golang中的sync.Once实现单例模式 Go语言并发环境下安全初始化

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

相关文章