单例模式
问题
Go 中如何实现单例模式?为什么推荐用 sync.Once?
答案
sync.Once 实现(推荐)
type Database struct {
conn *sql.DB
}
var (
dbInstance *Database
dbOnce sync.Once
)
func GetDB() *Database {
dbOnce.Do(func() {
conn, err := sql.Open("mysql", "dsn...")
if err != nil {
log.Fatal(err)
}
dbInstance = &Database{conn: conn}
})
return dbInstance
}
为什么推荐 sync.Once
- 线程安全:内部用
atomic+Mutex,保证只执行一次 - 零配置:不需要双重检查锁
- 性能好:初始化后走
atomic.Load快速路径,无锁
其他实现方式对比
包级变量 init(饿汉式):
var db = mustInitDB() // 程序启动时就初始化
func mustInitDB() *Database {
conn, err := sql.Open("mysql", "dsn...")
if err != nil {
log.Fatal(err)
}
return &Database{conn: conn}
}
| 方式 | 懒加载 | 线程安全 | 推荐 |
|---|---|---|---|
sync.Once | ✅ | ✅ | ⭐⭐⭐ |
init() / 包变量 | ❌(启动时) | ✅ | ⭐⭐ |
| 加锁检查 | ✅ | ✅ | ⭐ |
常见面试问题
Q1: sync.Once 的 Do 内部 panic 了会怎样?
答案:sync.Once 标记为"已完成",后续调用 Do 不会再执行。即使第一次 panic 了,单例也不会被重新初始化。如果需要重试初始化,不能用 sync.Once,需要自己加锁重试。
Q2: Go 中单例模式在哪些场景使用?
答案:
- 数据库连接池:
sql.DB本身就是连接池,只需一个实例 - 日志器:全局 Logger
- 配置对象:load 一次全局共享
- HTTP Client:复用连接的
http.Client