

新闻资讯
技术学院Go 的 sync.Once 是单例初始化首选,因其线程安全、无反射开销、自动处理双重检查锁;需配合错误返回、指针类型包变量及懒加载实现,避免并发初始化或忽略失败。
sync.Once 是单例初始化的首选Go 没有类和构造函数,所谓“单例”本质是全局唯一实例 + 一次初始化。直接用包级变量加 sync.Once 是最简洁、线程安全且无反射开销的方式。不用 init() 是因为它无法捕获错误;不用双重检查锁(DCL)是因为 Go 的 sync.Once 已经高效封装了该逻辑,手动实现反而容易出错。
常见错误:在多个 goroutine 中并发调用未加保护的初始化函数,导致多次实例化或 panic;或误用 new() / &T{} 直接赋值包变量,绕过初始化逻辑。
sync.Once 保证 Do() 中的函数只执行一次,即使多个 goroutine 同时调用*Config),避免值拷贝破坏单例语义典型场景是配置加载、数据库连接池、日志器等需延迟初始化且全局复用的资源。必须支持初始化失败回退,不能静默忽略错误。
package singleton
import (
"sync"
)
type Config struct {
Host string
Port int
}
var (
configInstance *Config
configOnce sync.Once
configErr error
)
func GetConfig() (*Config, error) {
configOnce.Do(func() {
// 模拟可能失败的初始化逻辑
c := &Config{Host: "localhost", Port: 8080}
// 假设这里校验 Port 是否合法
if c.Port <= 0 {
configErr = &ConfigError{"invalid port"}
return
}
configInstance = c
})
return configInstance, configErr
}
type ConfigError struct {
msg string
}
func (e *ConfigError) Error() string { return e.msg }
注意:configOnce.Do() 内部不抛 panic,而是通过闭包外的 configErr 传出错误;调用方必须检查返回的 e
rror,不能只判空指针。
当单例需要多种初始化策略(如从文件、环境变量、默认值),或需支持测试时替换依赖,把单例逻辑收进结构体更可控。此时“单例”不再是包级变量,而是由使用者显式创建并传递的唯一实例。
常见误区:把 sync.Once 放在结构体字段里,却让多个结构体实例共享同一个 Once —— 这违反单例本意;或者在方法里每次都 new 一个新 sync.Once,失去同步效果。
sync.Once 和实例字段放在同一结构体内,确保生命周期一致单例对象本身线程安全 ≠ 其字段线程安全。例如 map 或切片若被多个 goroutine 读写,仍会 panic。
典型错误:单例中定义 cache map[string]string,然后在 Get() / Set() 方法中直接操作,没加锁或用 sync.Map。
sync.RWMutex 保护读写,或改用 sync.Map(适用于读多写少)真正难的不是写出来,而是想清楚哪些状态必须全局唯一、哪些只是方便复用;还有就是——初始化失败时,你的调用方真的会检查 error 吗?