sync.once是并发安全延迟加载的唯一合理选择,因其用原子操作+互斥锁保障首次执行、后续直接返回,并建立happens-before确保可见性;手写双检锁或rwmutex易出错且无收益。

直接用 sync.Once 就行,它专为这个场景设计,不是“可以试试”,而是“唯一推荐的轻量解法”。其他方案(比如手写双检锁、包一层 sync.RWMutex)容易出错,且没额外收益。
为什么 sync.Once 是并发安全延迟加载的唯一合理选择
sync.Once 的 Do 方法内部用原子操作 + 互斥锁双重保障:首次调用时加锁执行函数,后续调用直接返回,且建立严格的 happens-before 关系,确保初始化完成后的字段对所有 goroutine 可见。它不依赖外部锁、不暴露状态、不需你判断 nil 或重试逻辑——这些恰恰是手写方案最容易翻车的地方。
- 常见错误:在方法里写
if p.real == nil { p.real = new(Real) }—— 并发下会触发多次初始化,甚至 panic - 更糟错误:用
sync.RWMutex读锁包裹检查 + 写锁包裹初始化,但漏掉双重检查(double-check),仍可能重复执行 -
sync.Once天然规避上述问题,代码少、行为确定、标准库兜底
带错误处理的延迟加载必须拆开存储 error 和 instance
初始化可能失败(比如数据库连不上、配置缺失),而 sync.Once.Do 不返回值。硬塞 panic 会让后续所有调用卡死或崩溃。正确做法是把 error 和实例指针都作为包级变量显式保存。
- 定义两个变量:
var (instance *DBConn; initErr error; once sync.Once) - 在
Do回调里做实际初始化,并赋值给这两个变量 - 对外函数统一返回
(*DBConn, error),调用方自然感知失败 - 注意:
initErr一旦非 nil,后续调用不会再重试——这是预期行为,避免反复冲击下游
HTTP 代理等真实场景中,不能只靠 sync.Once
当延迟加载目标是外部服务(如后端 HTTP client、gRPC 连接池),sync.Once 的“一锤子买卖”反而成缺陷:初始化失败后永远无法恢复。此时必须升级为带重试、超时、降级能力的懒加载器。
- 用
lazy.Group(来自golang.org/x/sync)替代sync.Once,它的Load方法可多次调用,失败后下次还能重试 - 初始化函数应返回
(T, error),失败时返回具体错误,便于日志和监控 - 别在
Do回调里做耗时操作(如读大文件、同步拉取远程配置),否则首次调用会被阻塞 - 如果初始化涉及健康检查(比如 ping DB),检查逻辑必须放在初始化函数内,不能靠“初始化完再另起 goroutine 检查”
真正容易被忽略的点:延迟加载不是“省代码”,而是“控时机+管生命周期”。sync.Once 解决了并发安全这一个点,但资源是否可重试、失败后怎么兜底、连接要不要主动 close,这些得靠业务逻辑补全,不能指望一个 Once 自动搞定。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











