Go单例靠包级变量+sync.Once实现,线程安全且延迟初始化;不用init因无法按需、不支持错误返回、难测试;禁用if-nil手动实现以防竞态。

Go 语言里没有“类”和“构造函数”,所以单例不是靠私有化构造器实现的,而是靠包级变量 + sync.Once 控制初始化时机 —— 这是最安全、最常用的方式。
用 sync.Once 保证全局唯一实例
直接声明一个包级指针变量,配合 sync.Once 的 Do 方法确保 init 函数只执行一次。这是 Go 官方推荐的单例写法,线程安全且无竞态风险。
-
sync.Once内部使用原子操作和互斥锁,比手写if instance == nil+mutex.Lock()更可靠 - 初始化逻辑放在闭包或独立函数里,避免在变量声明时就执行(防止 init 循环或依赖未就绪)
- 返回指针类型,方便后续方法定义为值接收者或指针接收者
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,不可控 - 无法在测试中重置或替换单例(比如 mock 数据库连接)
- 如果单例依赖其他尚未 init 的包,会触发隐式依赖顺序问题
带错误返回的单例(如初始化 DB 连接)
当初始化可能失败(比如打开文件、连接 Redis),需要把 error 暴露给调用方,并缓存失败状态避免重复尝试。
立即学习“go语言免费学习笔记(深入)”;
- 用两个包级变量:一个存实例,一个存 error
- 首次调用时初始化,之后直接返回缓存的结果(无论成功或失败)
- 不建议在
GetXXX()中 panic,应由上层决定如何处理 error
package 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
}
注意:不要用全局变量 + if 判断模拟单例
这种写法看似简洁,但存在竞态风险,尤其在高并发场景下可能创建多个实例:
// ❌ 危险!可能创建多个实例
var instance *Config
func GetConfig() *Config {
if instance == nil { // 多个 goroutine 同时通过判断
instance = &Config{} // 多次赋值
}
return instance
}
即使加了 mutex,也容易漏锁或死锁;而 sync.Once 是标准库专为此设计的原语,无需自己造轮子。真正要注意的是:别在单例方法里做耗时操作(比如每次调用都查一次 etcd),单例只管“实例创建”,不负责“每次调用逻辑”。










