sync.once 是 go 中实现线程安全单例最可靠的方式,因其确保初始化函数仅执行一次且支持按需延迟初始化、错误处理和参数传递,而 init() 无法满足这些需求。

sync.Once 是 Go 语言中创建线程安全单例最可靠的方式,其他方式要么不安全、要么有隐藏缺陷。别用 if instance == nil 加锁就以为万事大吉——漏掉双重检查或锁粒度不对,照样会创建多个实例。
为什么不用 init() 做单例?
因为 init() 在包加载时执行,无法按需延迟初始化,也不支持传参或错误处理。一旦在 init() 里读配置、连数据库或启动服务,失败会导致整个程序启动崩溃,且无法重试或降级。
常见误用:init() 中调用 sql.Open() 或 http.ListenAndServe()——这些操作本该由显式的 GetXXX() 函数控制生命周期。
sync.Once 必须是包级变量或结构体字段
它不能放在函数内部,否则每次调用都新建一个 sync.Once 实例,完全失去“只执行一次”的意义。
正确写法:
var once sync.Once
var instance *Service
func GetService() *Service {
once.Do(func() {
instance = &Service{...}
})
return instance
}
错误写法(局部变量):
func GetService() *Service {
var once sync.Once // ❌ 每次调用都是新 once
once.Do(...)
...
}
-
sync.Once是值类型,但必须持久存在;传参或赋值给局部变量会复制,失效 - 如果单例属于某个客户端结构体,推荐把
once sync.Once作为字段嵌入,但方法接收者必须是指针:func (c *Client) Do() {...} - 初始化函数若 panic,
sync.Once会永久记住失败状态,后续调用仍 panic——需自行兜底,比如用返回值 + error 判断
带参数的单例怎么写?
全局变量硬编码参数(如 var db *sql.DB + func InitDB(dsn string))破坏封装、不可测试、无法复用。
正确做法是把构造逻辑收进函数,单例控制交给 sync.Once:
type Config struct{ DSN string; Timeout int }
var configOnce sync.Once
var configInstance *Config
func NewConfig(cfg Config) *Config {
configOnce.Do(func() {
configInstance = &cfg // 或深拷贝、校验等
})
return configInstance
}
func GetConfig() *Config {
return configInstance
}
- 测试时可直接调用
NewConfig(testCfg)构造新实例,不污染全局状态 - 避免用
var globalCfg Config然后在init()里赋值——参数被固化,无法动态切换 - 如果需要多次初始化不同配置的单例(如多 DB 实例),就别用全局单例,改用对象池或依赖注入
饿汉式和懒汉式性能差异其实不重要
饿汉式(包级变量直接初始化)看似快,但代价是:资源提前占用、初始化失败无法感知、无法按需加载。
懒汉式配合 sync.Once 的首次调用开销几乎可以忽略,后续调用零成本——这才是真实场景下的合理取舍。
真正容易被忽略的是:单例初始化函数里是否做了阻塞 I/O、是否忽略了 error 处理、是否把 sync.Once 当成了万能锁来用(它只保证一次,不保护读写并发)。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











