sync.once.do只执行一次,因其用uint32标志位配合atomic.compareandswapuint32保证仅一个goroutine能成功标记“已执行”,其余直接返回;panic会导致该once永久失效。

sync.Once.Do 为什么只执行一次
因为 sync.Once 内部用一个 uint32 标志位记录是否已执行,Do 方法通过原子操作(atomic.CompareAndSwapUint32)确保最多只有一个 goroutine 能成功写入“已执行”状态;其余并发调用会直接返回,不重试、不阻塞、不报错。
常见错误现象:在 Do 的回调函数里 panic,会导致后续所有调用都 panic —— Once 不捕获异常,也不重置状态。一旦失败,该 Once 实例永久失效。
- 必须把初始化逻辑封装进无返回值函数,且自行处理内部 panic(比如 recover)
- 不要在
Do里做需要重试的操作(如网络请求失败后想再试一次),它不具备重试语义 - 多个
Once实例之间完全独立,不能靠它协调跨变量的初始化顺序
单例对象初始化时如何安全返回值
sync.Once 的 Do 方法签名是 func(f func()),不接受参数也不返回值。所以“获取单例实例”和“初始化”必须拆成两步:先声明包级变量,再在 Do 回调里赋值。
典型写法是用指针或接口类型变量承载初始化结果,避免竞态:
立即学习“go语言免费学习笔记(深入)”;
var (
instance *MyService
once sync.Once
)
func GetInstance() *MyService {
once.Do(func() {
instance = &MyService{...} // 这里完成构造、依赖注入、资源申请等
})
return instance
}
- 不能把
instance声明为值类型并直接赋值(如instance = MyService{...}),若结构体较大,可能触发多次复制 - 如果单例需要实现接口,建议返回接口类型变量(如
var instance Service),但初始化仍需在Do中完成具体赋值 - 初始化函数中若涉及全局状态修改(如注册 handler、启动 goroutine),要确认这些操作本身是幂等的
和 init 函数比,Once 延迟初始化适合什么场景
init 在包加载时立即执行,无法按需延迟;sync.Once 把初始化推迟到第一次调用时,适用于:启动慢、依赖外部条件、或并非所有运行路径都需要的对象。
比如数据库连接池、HTTP 客户端、配置解析器 —— 它们可能依赖环境变量、远程配置服务或命令行参数,而这些在 init 阶段尚未就绪。
- 若初始化过程可能失败(如配置缺失、端口被占),用
Once可以把错误暴露给业务层处理;init中 panic 会导致整个程序退出 - 测试时容易 mock:你可以控制“第一次调用”时机,甚至在测试前手动调用
Getxxx()触发初始化,避免测试并发干扰 - 注意性能差异:
Once.Do每次调用都有原子读开销(极小),但远小于重复初始化的代价;对高频调用路径,确保你不是在循环里反复调用GetInstance()却误以为每次都在初始化
多个 Once 实例共用一个初始化函数容易踩什么坑
有人为了复用逻辑,写类似 once1.Do(initFunc); once2.Do(initFunc),但这是危险的 —— 如果 initFunc 修改了共享状态(比如全局 map、文件句柄、未加锁的计数器),并发执行时可能出问题,因为 Do 只保证“单个 Once 实例内只执行一次”,不保证多个实例之间的执行互斥。
- 每个
sync.Once实例只保护自己的回调执行,彼此无关 - 若初始化逻辑有副作用且需全局唯一性(例如只启动一个后台 goroutine),必须用同一个
Once实例来 guard,而不是多个 - 更隐蔽的坑:回调函数引用了闭包变量,而该变量在不同
Do调用间被多次修改,导致行为不一致
真正复杂的初始化往往需要组合多个 Once 或配合 sync.OnceValue(Go 1.21+)—— 但后者返回值是只读的,且不支持 error,得自己包装一层。










