Go单例靠包级变量+sync.Once实现,线程安全且延迟初始化;不用init因无法按需、不支持错误返回、难测试;禁用if-nil手动实现以防竞态。
Go 语言里没有“类”和“构造函数”,所以单例不是靠私有化构造器实现的,而是靠包级变量 + sync.Once 控制初始化时机 —— 这是最安全、最常用的方式。
sync.Once 保证全局唯一实例直接声明一个包级指针变量,配合 sync.Once 的 Do 方法确保 init 函数只执行一次。这是 Go 官方推荐的单例写法,线程安全且无竞态风险。
sync.Once 内部使用原子操作和互斥锁,比手写 if instance == nil + mutex.Lock() 更可靠
package singleton
import "sync"
type Config struct {
Timeout int
Env string
}
var (
instance *Config
once sync.Once
)
func GetConfig() *Config {
once.Do(func() {
instance = &Config{
Timeout: 30,
Env: "prod",
}
})
return instance
}
init() 函数?init() 确实只执行一次,但它在包加载时就运行,无法按需延迟初始化,也不支持带参数或错误返回 —— 实际项目中单例常需读配置、连数据库、校验权限,这些都可能失败。
init() 不能返回 error,出错只能 panic,不可控当初始化可能失败(比如打开文件、连接 Redis),需要把 error 暴露给调用方,并缓存失败状态避免重复尝试。
GetXXX() 中 panic,应由上层决定如何处理 errorpackage db
import (
"database/sql"
"sync"
)
var (
instance *sql.DB
err error
once sync.Once
)
func GetDB(dsn string) (*sql.DB, error) {
once.Do(func() {
instance, err = sql.Open("mysql", dsn)
if err == nil {
err = instance.Ping()
}
})
return instance, err
}
这种写法看似简洁,但存在竞态风险,尤其在高并发场景下可能创建多个实例:
// ❌ 危险!可能创建多个实例
var instance *Config
func GetConfig() *Config {
if instance == nil { // 多个 goroutine 同时通过判断
instance = &Config{} // 多次赋值
}
return instance
}
即使加了 mutex,也容易漏锁或死锁;而 sync.Once 是标准库专为此设计的原语,无需自己造轮子。真正要注意的是:别在单例方法里做耗时操作(比如每次调用都查一次 etcd),单例只管“实例创建”,不负责“每次调用逻辑”。