sync.Once.Do只能执行一次,因其内部用uint32类型的done字段配合原子操作和互斥锁实现状态机:首次调用时原子检查done为0,加锁后双重校验并执行函数,随后原子置done为1;后续所有调用均直接返回,不加锁、不执行、不重试,即使函数panic也如此。

sync.Once.Do 为什么只能执行一次
sync.Once 的核心是内部的 done 字段(uint32)和一个互斥锁。调用 Do 时先原子读取 done,为 0 才尝试加锁并再次检查;一旦设为 1,后续所有调用都直接返回,不进锁、不执行函数。这不是靠“记录是否调过”实现的,而是靠原子状态机——所以即使传入的函数 panic,done 仍会被设为 1,后续调用也不会重试。
常见错误是误以为 panic 后还能重试:
var once sync.Once
once.Do(func() {
panic("init failed")
})
once.Do(func() { // 这个不会执行!
fmt.Println("retry")
})
延迟加载单例对象的典型写法
最常用场景是全局配置、数据库连接池或第三方客户端初始化。关键点不是“只做一次”,而是“首次访问时才创建”,避免程序启动时就阻塞或失败。
- 把初始化逻辑包在匿名函数里传给
Do,不要提前调用 - 用指针或接口变量保存结果,避免值拷贝开销
- 如果初始化可能失败,必须在外层处理错误(
sync.Once本身不返回 error)
示例:
var (
client *http.Client
once sync.Once
err error
)
<p>func GetHTTPClient() (<em>http.Client, error) {
once.Do(func() {
client = &http.Client{Timeout: 30 </em> time.Second}
// 这里可加入健康检查,失败则赋值 err
if !isNetworkAvailable() {
err = errors.New("network unreachable")
}
})
return client, err
}</p>
多个依赖项怎么用 Once 协调初始化顺序
当 A 依赖 B,B 又依赖 C,不能简单套多层 Do,否则可能死锁或状态错乱。正确做法是:每个资源独立用 sync.Once,并在各自初始化函数中按需调用上游的获取函数。
例如日志组件依赖配置,配置又依赖环境变量解析:
var (
configOnce sync.Once
config *Config
<pre class="brush:php;toolbar:false;">loggerOnce sync.Once
logger *zap.Logger
)
func GetConfig() *Config { configOnce.Do(func() { config = &Config{Env: os.Getenv("ENV")} }) return config }
func GetLogger() *zap.Logger { loggerOnce.Do(func() { cfg := GetConfig() // 安全:GetConfig 内部有自己 Once logger = zap.NewExample().Named(cfg.Env) }) return logger }
注意:不能在 GetConfig 的 Do 函数里调用 GetLogger,否则循环依赖会卡死。
Once 和 lazy sync.OnceValue 的区别(Go 1.21+)
sync.OnceValue 是 Go 1.21 新增的替代方案,它把“执行 + 返回值”合并成一个原子操作,更贴合延迟加载语义。而 sync.Once 只管执行,返回值得靠外部变量传递,容易出现竞态(比如忘记加锁读取结果变量)。
如果你用的是 Go 1.21 或更高版本,优先选 sync.OnceValue:
var clientOnce sync.OnceValue
<p>func GetHTTPClient() <em>http.Client {
v, _ := clientOnce.Do(func() any {
return &http.Client{Timeout: 30 </em> time.Second}
})
return v.(*http.Client)
}</p>
但要注意:OnceValue.Do 返回 any,类型断言失败会 panic;且它不支持返回 error,错误仍需在闭包内处理并存到全局变量。
真正容易被忽略的是:sync.Once 不提供内存屏障以外的同步保证——它不确保你初始化的结构体字段对其他 goroutine 立即可见(虽然实际中极少出问题)。如果字段涉及非安全操作(如 unsafe.Pointer),得额外加 runtime.GC() 或使用 atomic.StorePointer 配合。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











