Go sync.Once 使用指南:单次初始化的最佳实践

一、为什么需要 sync.Once?
在 Go 并发编程中,单次初始化是常见需求场景(如配置加载、数据库连接池创建等)。sync.Once
是 Go 标准库提供的线程安全工具,能确保某段代码逻辑在整个程序生命周期中仅执行一次,与 init()
函数的区别在于它支持懒加载模式。
二、基础用法与原理
- 核心方法
type Once struct {
done uint32 // 原子操作标记位
m Mutex // 互斥锁
}
func (o *Once) Do(f func())
- 极简示例
var (
instance *Singleton
once sync.Once
)
func GetInstance() *Singleton {
once.Do(func() {
instance = &Singleton{Data: "已初始化"}
})
return instance
}
-执行效果:无论并发调用多少次 GetInstance(),实例只会创建一次*
三、典型应用场景
场景 1:全局配置加载
var (
appConfig *Config
loadOnce sync.Once
)
func LoadConfig() *Config {
loadOnce.Do(func() {
data, _ := os.ReadFile("config.yaml")
appConfig = parseYAML(data) // 真实项目需处理错误
})
return appConfig
}
场景 2:数据库连接池初始化
var (
dbPool *sql.DB
dbOnce sync.Once
)
func GetDB() *sql.DB {
dbOnce.Do(func() {
connStr := "user:pass@/dbname"
dbPool, _ = sql.Open("mysql", connStr)
dbPool.SetMaxOpenConns(100)
})
return dbPool
}
场景 3:单例模式实现
type Logger struct {
// 日志器字段
}
var (
loggerInstance *Logger
loggerOnce sync.Once
)
func GetLogger() *Logger {
loggerOnce.Do(func() {
loggerInstance = &Logger{
// 初始化字段
}
loggerInstance.Init()
})
return loggerInstance
}
四、高级技巧与注意事项
- 错误处理新方案
使用新增的
sync.OnceValue
和sync.OnceValues
实现带错误返回:
var loadConfigOnce = sync.OnceValues(func() (*Config, error) {
data, err := os.ReadFile("config.yaml")
if err != nil {
return nil, err
}
return parseYAML(data)
})
func GetConfig() (*Config, error) {
return loadConfigOnce()
}
- 闭包陷阱规避 (1.23版本后有变化)
// ❌ 错误写法(闭包捕获循环变量)
for _, url := range urls {
go func() {
once.Do(func() {
fetch(url) // 实际只会执行最后一次循环的url
})
}()
}
// ✅ 正确写法
for _, url := range urls {
u := url // 创建局部变量副本
go func() {
once.Do(func() {
fetch(u)
})
}()
}
- 性能优化建议
- 高频调用场景建议将
Once
实例声明为全局变量 - 避免在
Do()
方法中执行耗时操作(会阻塞其他协程) - 重置需求可通过
atomic.StoreUint32(&once.done, 0)
实现(需谨慎使用)
