sync.Once.Do 通过 uint32 状态字段和原子操作确保函数只执行一次,首次调用者执行,其余自旋等待;若函数 panic,Once 认为已执行完毕,不重试;需在闭包内完成全部初始化,避免捕获外部变量。

sync.Once.Do 为什么能保证只执行一次
sync.Once.Do 内部用一个 uint32 状态字段 + 原子操作控制执行流程,不是靠锁阻塞所有 goroutine,而是让首次调用者真正执行函数,其余并发调用者自旋等待直到完成。这意味着即使 100 个 goroutine 同时调用 Do,也只会触发一次初始化逻辑。
注意:它不保证“立即返回”,只保证“最终只执行一次”;被包装的函数若 panic,sync.Once 会认为已执行完毕(不会重试),后续调用直接返回——这点常被忽略。
典型误用:把 newDB 或 newCache 写在 Do 参数里
常见错误是把构造逻辑直接写进 Do 的闭包中,却没意识到闭包捕获变量可能导致非预期行为:
// ❌ 错误:db 可能为 nil,且每次调用都新建 *sql.DB 实例(但只保留最后一次赋值)
var db *sql.DB
once.Do(func() {
db = newDB() // newDB() 每次都执行,只是结果被覆盖
})
正确做法是把初始化逻辑完全封装在闭包内,并确保副作用只发生一次:
// ✅ 正确:newDB() 仅执行一次,db 被安全赋值
var db *sql.DB
once.Do(func() {
db = newDB() // 这行只运行一次
})
- 不要在
Do外声明变量再赋值,而应在闭包内完成全部初始化和赋值 - 避免闭包引用外部可变变量(如循环变量 i),否则可能捕获到错误值
- 如果初始化需要返回多个值(如
conn, err),建议用全局变量或结构体字段接收,不要依赖闭包外的临时变量
与 init 函数、sync.Once 的适用边界对比
init 在包加载时执行,无法按需延迟;sync.Once 是运行时按第一次调用才触发,适合依赖配置、环境变量或网络资源的场景。
商淘云B2B2C多用户商城系统是一款基于国内大众化框架打造的B2B2C电商平台,是目前完善度领先的电商管理平台标准化产品,系统主要功能采用高内聚,辅助功能插件式实现,全系统拥有PC、手机H5、微商城、买家安卓端APP、买家苹果端APP、卖家安卓端APP、卖家苹果端APP、微信小程序,支持可视化装修,另有无缝对接的商淘源码IM客服系统,极其适合中小型企业快速上线商务平台。
- 需要读取
os.Getenv("DB_URL")才连接数据库?→ 必须用sync.Once - 初始化涉及 HTTP client 或 gRPC conn,且可能失败需重试?→
sync.Once不重试,得自己在外层加错误处理和重试逻辑 - 纯内存结构(如 map、slice)预分配?→
init更轻量,无需并发控制 - 多个初始化步骤有依赖顺序(如先连 DB 再建 cache)?→ 用一个
sync.Once包裹整个流程,别拆成多个
Once 实例该定义成全局变量还是结构体字段
绝大多数情况应该定义为**包级全局变量**,比如 var once sync.Once。定义在结构体里只有在以下情况才合理:
- 每个 struct 实例需要独立的一次性初始化(如 per-connection 的 TLS config 缓存)
- 结构体本身生命周期明确,且初始化逻辑强绑定该实例(例如某个 worker 初始化其专属的 channel buffer)
反例:把 sync.Once 放在 HTTP handler 结构体里,却在每个请求中调用 Do——这等于每个请求都 new 一个 Once,完全失去意义。
容易被忽略的是:Once 不可复制。如果结构体包含 sync.Once 字段,该结构体不能被赋值或作为 map value 直接存储(会触发 panic)。必须用指针传递或确保只取地址使用。









