Go中实现线程安全单例应优先使用sync.Once,它保证初始化函数仅执行一次且自带同步语义;避免手动实现双重检查锁定(DCL),因其在Go中冗余、易错且不安全;支持带错误返回的初始化需配合全局err变量与once.Do闭包。

在 Go 语言中实现线程安全的单例模式,核心是避免多个 goroutine 同时初始化实例导致重复创建或竞态。Go 提供了简洁高效的原语(如 sync.Once)来解决这个问题,无需手动加锁或双重检查锁定(DCL)——后者在 Go 中不仅冗余,还容易出错。
用 sync.Once 实现最简线程安全单例
sync.Once 保证其包裹的函数只执行一次,且自带同步语义,天然适合单例初始化场景。这是 Go 官方推荐、最可靠的方式。
- 定义一个私有全局变量(如
instance *Singleton)和一个sync.Once实例 - 提供一个公开的获取函数(如
GetInstance()),内部调用once.Do()来惰性创建实例 - 所有并发调用
GetInstance()都会阻塞直到首次创建完成,之后直接返回已创建的实例
示例代码:
var (
instance *Singleton
once sync.Once
)
type Singleton struct {
data string
}
func GetInstance() *Singleton {
once.Do(func() {
instance = &Singleton{data: "initialized"}
})
return instance
}
避免常见陷阱:不要用双重检查锁定(DCL)
有些开发者习惯从 Java 或 C++ 移植 DCL 写法(先判空 → 加锁 → 再判空 → 初始化),但在 Go 中这既不必要也不安全:
立即学习“go语言免费学习笔记(深入)”;
-
sync.Once已经高效处理了“仅一次”和内存可见性问题 - 手动实现 DCL 容易因缺少内存屏障(如
atomic.StorePointer)导致读取到未完全构造的对象 - 代码更长、可读性差、维护成本高
除非有特殊需求(如需延迟初始化前做复杂条件判断),否则坚决不用 DCL。
支持带参数或错误的初始化
如果单例初始化可能失败(如连接数据库、读配置),可将初始化逻辑封装为一个私有函数,返回实例和 error,并在 once.Do 中调用它。注意:error 本身不能通过 sync.Once 直接暴露,需额外存储。
- 声明
var err error全局变量配合once - 在
once.Do的闭包中执行初始化并赋值instance和err - 对外提供
GetInstance()和InitError()两个函数,或统一返回(*Singleton, error)
这样既保持线程安全,又支持错误传播。
测试并发安全性很简单
写一个并发调用 GetInstance() 的测试,验证是否始终返回同一地址、且初始化逻辑只执行一次:
func TestSingletonConcurrent(t *testing.T) {
var wg sync.WaitGroup
instances := make([]*Singleton, 100)
for i := 0; i < 100; i++ {
wg.Add(1)
go func(j int) {
defer wg.Done()
instances[j] = GetInstance()
}(i)
}
wg.Wait()
// 所有指针应相等
for i := 1; i < len(instances); i++ {
if instances[i] != instances[0] {
t.Fatal("not singleton!")
}
}
}
基本上就这些 —— 简洁、安全、符合 Go 习惯。










