go中单例本质是全局变量加初始化控制,需满足懒加载、线程安全、可测试,标准做法是sync.once配合指针变量;应避免init()、手动加锁、返回值拷贝及硬编码依赖,推荐依赖注入;sync.once不支持失败重试,http中勿将request级对象误作单例。
Go里没有“new Singleton()”这种单例写法
Go语言本身不支持类和构造函数,所谓“单例”本质是**全局变量 + 初始化控制**。直接声明一个包级变量 var instance *MyService 不等于单例——它可能未初始化、被并发读写、或在测试中无法重置。
真正可靠的单例必须满足三点:懒加载、线程安全、可测试。标准做法是用 sync.Once 配合指针变量:
var (
instance *MyService
once sync.Once
)
func GetInstance() *MyService {
once.Do(func() {
instance = &MyService{...}
})
return instance
}
- 不用
init():它在包加载时执行,无法按需初始化,且无法捕获初始化失败的错误 - 避免在
GetInstance()里加锁:sync.Once已保证只执行一次,比手写互斥锁更轻量 - 不要返回值拷贝(如
func GetInstance() MyService):会导致每次调用都复制结构体,失去单例语义
依赖注入场景下硬编码单例会破坏可测试性
很多项目在 handler 或 service 层直接调 database.GetInstance() 或 cache.NewRedisClient(),看似方便,实则让单元测试难以 mock 依赖。
正确做法是把单例作为依赖传入,而非内部获取:
type UserService struct {
db *sql.DB // 而不是自己调 database.GetInstance()
}
func NewUserService(db *sql.DB) *UserService {
return &UserService{db: db}
}
- 测试时可传入
sqlmock或内存数据库实例,无需启动真实 DB - 多个环境(dev/staging/prod)可注入不同配置的实例,而不是靠包变量切换
- 如果真需要“全局可用”,可在 main 包统一创建并注入,而非每个子包自行获取
sync.Once 不是万能的——初始化失败时无法重试
sync.Once 的设计是“最多执行一次”,一旦 Do() 内部 panic 或返回错误,后续调用永远拿不到有效实例,且无提示。