sync.Once是Go标准库唯一推荐的线程安全延迟初始化方式,通过原子操作和互斥锁确保初始化函数仅执行一次,适用于数据库连接、配置加载等场景。

Go 语言里没有 lazy 关键字,延迟加载必须靠手动控制初始化时机,核心就一条:别在结构体字段里直接 new 实例,得用指针 + 同步机制兜底。
用 sync.Once 做最简安全延迟初始化
这是标准库唯一推荐的并发安全方式,适合绝大多数场景——数据库连接、配置加载、大对象构建等。
-
sync.Once内部已做原子控制,无需自己加锁或双检 - 初始化函数只执行一次,即使多个 goroutine 同时触发
Do()也无 panic 风险 - 错误处理要显式存到字段里,否则第二次调用会返回 nil 而不是上一次的 error
示例:
var (
db *sql.DB
once sync.Once
initErr error
)
func GetDB() (*sql.DB, error) {
once.Do(func() {
db, initErr = sql.Open("mysql", "user:pass@/test")
if initErr != nil {
return
}
initErr = db.Ping()
})
return db, initErr
}
Proxy 模式下延迟加载的真实结构体字段怎么设
Proxy 不是装饰器语法糖,它是个包装结构体,真实对象必须声明为指针且初始为 nil,否则延迟失效。
立即学习“go语言免费学习笔记(深入)”;
- 字段类型必须是
*RealService,不能是RealService(否则构造时就实例化了) - 绝对不要嵌入
RealService,否则未受控方法会暴露出去 - 所有方法都应先检查指针是否为
nil,再调用once.Do()初始化
错误写法:p.real = RealLoader{path: "/tmp"} —— 这行代码一执行,就没了“延迟”。
defer 和延迟加载容易混在一起,但它们解决的是不同问题
defer 是函数退出时执行清理,和“对象何时创建”无关;延迟加载关注的是“实例第一次被需要时才生成”。两者常共存,但逻辑边界必须清晰。
- 比如
GetDB()返回的*sql.DB,后续调用Query()时仍需defer rows.Close(),这是资源释放,不是延迟加载 - 如果在初始化函数里打开文件并忘记
defer f.Close(),会导致句柄泄漏,和sync.Once是否用对无关 - 命名返回值 +
defer可以包装 error,但不能用来延迟初始化——defer执行时,初始化早已完成
初始化函数里不能塞耗时或阻塞操作
sync.Once.Do() 是同步阻塞的,首次调用者会卡住直到初始化结束。线上服务若在这里读 GB 级配置文件或连慢接口,会拖垮整个请求链路。
- 初始化函数应只做必要最小动作:建连接、校验健康、设置基础字段
- 重计算、预热缓存、批量加载等应放到初始化之后异步做(比如起个 goroutine)
- 如果初始化可能失败,错误必须保存,否则第二次调用会 panic 或返回 nil 指针
真正难的不是写几行 once.Do,而是判断哪些该延迟、哪些该预热、哪些根本不该放进来——这取决于你对资源生命周期和调用频次的真实观测。


















