Go中推荐用包级变量+sync.Once实现线程安全、惰性初始化单例,避免init阶段panic;Go 1.21起可用sync.OnceValue简化写法;多配置、需mock、短生命周期等场景应避免单例。

Go 里没有语言级的“单例类”概念,但可以通过包级变量 + sync.Once 实现线程安全、惰性初始化的单例——这是最常用也最推荐的方式。
为什么不用全局变量直接初始化?
直接声明包级变量(如 var instance *DB)会触发包加载时立即初始化,无法控制时机,且可能因依赖未就绪导致 panic;更重要的是,它不支持按需创建和错误处理。而真实项目中,单例常需连接数据库、读配置、启动协程等耗时或可能失败的操作。
常见错误现象:panic: runtime error: invalid memory address —— 因 init() 阶段依赖其他未初始化的包变量。
- 用
sync.Once可确保initFunc最多执行一次,且阻塞后续 goroutine 直到完成 - 初始化函数可返回 error,便于上层判断是否构建成功
- 避免在
init()函数中做复杂逻辑,把控制权交还给业务调用方
标准写法:带错误检查的惰性单例
典型结构包含私有结构体、私有指针变量、私有 sync.Once 和公开获取函数。关键点在于把创建逻辑封装进闭包,并在 Do() 中调用:
var (
instance *Config
once sync.Once
err error
)
type Config struct {
Timeout int
Env string
}
func GetConfig() (*Config, error) {
once.Do(func() {
instance, err = loadConfig() // 可能失败,比如文件不存在
})
return instance, err
}
func loadConfig() (*Config, error) {
// 模拟加载逻辑
return &Config{Timeout: 30, Env: "prod"}, nil
}
注意:once.Do() 不会传播 loadConfig() 的 panic,它只保证执行一次;若要捕获 panic,需在 loadConfig() 内部处理。
替代方案:sync.OnceValue(Go 1.21+)
Go 1.21 引入了 sync.OnceValue,更简洁且天然支持返回值(包括 error),底层自动处理 panic 捕获与重试隔离:
var configOnce sync.OnceValue
func GetConfig() (*Config, error) {
v, ok := configOnce.Do(func() (any, error) {
return loadConfig()
})
if !ok {
return nil, fmt.Errorf("failed to init config")
}
return v.(*Config), nil
}
但要注意:sync.OnceValue 在 Go 1.21 才可用,老版本项目不能用;它返回 any,需手动类型断言,容易写错类型;且无法像 sync.Once 那样复用同一个实例做多个不同初始化逻辑。
什么场景下不该用单例?
单例不是银弹。以下情况应避免:
- 需要多套独立配置的组件(如同时连两个 Redis 实例)——改用工厂函数
NewRedisClient(cfg) - 测试时需 mock 或替换行为(单例会让单元测试难隔离)——优先注入接口而非依赖全局实例
- 生命周期需受控(如 Web 请求结束就释放资源)——单例常驻内存,不适合短生命周期对象
- 结构体含非导出字段但没提供构造函数,又没加注释说明“仅限单例使用”——容易被误用为普通值
真正该用单例的,是那些开销大、全局唯一、且生命周期贯穿整个程序运行期的对象:日志器(log.Logger)、配置管理器、指标收集器(prometheus.Registry)、全局连接池(sql.DB)。










