Go单例靠sync.Once+包级指针变量实现,而非类或init();因init()无法延迟、容错和按需初始化,且不支持返回错误;误用值类型、忽略并发安全或滥用init()均会导致失效;非必要场景应避免单例以符合显式依赖原则。

Go 语言里没有“类”和“构造函数”,所谓单例不是靠语法限制,而是靠包级变量 + sync.Once 控制初始化时机来保证全局唯一实例——这是最稳妥、被标准库广泛采用的方式。
为什么不用 init() 函数做单例?
init() 确实能确保只执行一次,但它在包导入时就触发,无法延迟初始化,也无法处理带参数或可能失败的初始化逻辑(比如连接数据库、读配置文件)。一旦初始化出错,整个程序 panic,且无法重试。
更关键的是:init() 不支持返回错误,也不支持按需创建。实际项目中,你往往需要:
- 首次调用
NewClient()时才真正初始化 - 初始化失败时返回
error而非直接崩溃 - 后续调用直接复用已创建的实例
标准写法:用 sync.Once + 指针字段封装
核心是把实例声明为包级变量(var instance *Singleton),再用 sync.Once 保证 initFunc 只执行一次。注意必须是指针类型,否则每次调用都复制值,失去单例意义。
立即学习“go语言免费学习笔记(深入)”;
典型结构如下:
var (
instance *Singleton
once sync.Once
)
type Singleton struct {
Config string
}
func GetInstance() *Singleton {
once.Do(func() {
instance = &Singleton{Config: "default"}
})
return instance
}
如果初始化可能失败,可改用带 error 的版本:
var (
instance *Singleton
err error
once sync.Once
)
func GetInstance() (*Singleton, error) {
once.Do(func() {
instance, err = NewSingleton()
})
return instance, err
}
常见误操作:直接返回值类型或忽略并发安全
以下写法是错的:
- 写成
var instance Singleton(值类型)→ 每次GetInstance()返回副本,修改不共享 - 省略
sync.Once,只靠 if 判断instance == nil→ 在高并发下可能创建多个实例 - 把
sync.Once放在函数内部(如局部变量)→ 每次调用都新建一个Once,完全失效 - 在
init()中调用外部服务(如http.Get)→ 导入包即阻塞,且无法测试 mock
什么时候不该用单例?
单例容易掩盖依赖关系,让测试变难,也违背 Go “显式优于隐式”的哲学。以下场景建议绕开:
- 需要多套配置并存(如同时连两个 Redis 实例)→ 改用工厂函数
NewRedisClient(cfg Config) *Client - 单元测试中要替换行为(如 mock 数据库)→ 单例无法注入 mock,应通过参数传入接口
- 生命周期需要控制(如 Web 服务重启时重载配置)→ 单例无法销毁重建,应由上层管理生命周期
真正适合单例的,通常是无状态、全局共享、初始化成本高且无需变更的组件,比如日志器(log.Logger)、指标收集器(prometheus.Registerer)或配置解析器(只读配置树)。










