sync.Once 仅保证初始化逻辑执行一次,非单例模式本身;安全单例需结合包级变量、错误处理与非空保障,避免构造失败导致 panic 或静默错误。

sync.Once 不是单例模式本身,它只是确保某段初始化逻辑最多执行一次。真正在 Go 里落地一个安全、可用的单例,必须把它和包级变量、错误处理、非空保障一起用。
为什么直接 once.Do(func() { instance = &T{} }) 是危险的
这种写法看似简洁,但掩盖了三个关键问题:
- 构造函数可能失败(比如配置缺失、网络超时),
instance变成nil,后续调用直接 panic -
sync.Once.Do不捕获 panic,也不返回 error —— 它只管“是否已调用”,不管“调用是否成功” - 调用方无法区分“还没初始化”和“初始化失败”,错误被静默吞掉
正确封装:用结构体承载实例和错误
必须把初始化结果(成功实例或失败 error)统一存进一个私有结构体,并让获取函数返回 (*T, error):
var (
serviceOnce sync.Once
serviceInst struct {
instance *Service
err error
}
)
func GetService() (*Service, error) {
serviceOnce.Do(func() {
s, err := newService() // 这里做真实构造,可能返回 error
serviceInst = struct{ instance *Service; err error }{s, err}
})
return serviceInst.instance, serviceInst.err
}
注意:serviceInst 必须是单一结构体变量,不能拆成两个独立包级变量(instance 和 err),否则并发读写可能不一致。
立即学习“go语言免费学习笔记(深入)”;
别在 Do 里调用 log.Fatal 或 os.Exit
这类调用会终止整个进程,而其他正在等待 once.Do() 的 goroutine 会永远卡住,变成 goroutine 泄漏:
- Go 不会自动唤醒它们,也不会回收
- HTTP handler、gRPC server 等场景下,首请求卡住会导致整条链路超时
- 正确做法是把错误暴露出去,由上层决定重试、降级或告警
sync.Once 必须是包级变量或结构体指针字段
它一旦被复制(值拷贝、传参、局部声明),就失去意义:
- 错误写法:
func handler() { var once sync.Once; once.Do(...) }→ 每次调用都新建一个 - 错误写法:
type S struct { once sync.Once }→ 每次new(S)都得到全新状态 - 正确写法:包级声明
var once sync.Once,或结构体中存指针:once *sync.Once
最容易被忽略的是测试场景:复用全局 sync.Once 会导致第二个 test case 直接跳过初始化,mock 失效 —— 测试时应改用局部 sync.Once 或抽离初始化逻辑为可注入函数。


















