sync.once是go单例最稳妥的选择,因其通过原子操作+互斥锁确保初始化函数仅执行一次、所有goroutine阻塞等待完成,支持按需延迟初始化、错误处理与参数传递,而init()无法延迟、不支持传参和失败重试,手动加锁易出竞态。

为什么 sync.Once 是 Go 单例最稳妥的选择
因为 Go 没有类和构造函数,所谓“单例”本质是控制一个变量只被初始化一次。手动加锁(如 sync.Mutex)容易漏掉检查或重复初始化;用 init() 又无法延迟加载、不支持带参数的初始化。而 sync.Once 的 Do() 方法天然保证:无论多少 goroutine 并发调用,传入的函数只会执行一次,且阻塞后续调用直到完成——这正是单例初始化需要的语义。
常见错误是写成:if instance == nil { instance = newSomething() },这在并发下会创建多个实例。必须配合同步原语,sync.Once 是标准库里唯一为此设计的工具。
带初始化参数的单例怎么写
单例对象初始化往往依赖配置、连接字符串等运行时参数,不能放在包级变量声明时硬编码。正确做法是把参数通过闭包传给 sync.Once.Do() 的回调函数:
var (
instance *DB
once sync.Once
)
func GetDB(dsn string) *DB {
once.Do(func() {
instance = &DB{conn: sql.Open("mysql", dsn)}
})
return instance
}
注意:once 和 instance 必须是包级变量,否则每次调用 GetDB() 都会新建 sync.Once 实例,失去“只执行一次”的意义。另外,GetDB() 不能接受指针或结构体值作为参数传入闭包(会导致闭包捕获临时变量),字符串、整数等值类型最安全。
为什么不能用 init() 替代 sync.Once
init() 确实只执行一次,但它发生在包导入时、早于 main(),无法获取命令行参数、环境变量或配置文件内容。更关键的是:init() 无返回值、不能传参、无法重试或处理初始化失败(比如数据库连不上),一旦 panic 整个程序崩溃,且不可恢复。
典型误用场景:
- 想根据
os.Getenv("ENV")初始化不同环境的 client ——init()执行时环境变量可能还没加载完 - 初始化依赖另一个尚未
init()的包(导致初始化顺序不确定) - 测试时需要 mock 单例行为 ——
init()不可重置、不可覆盖
并发安全但不是线程安全?小心资源本身的竞态
sync.Once 只保障初始化过程的串行化,不保护单例对象后续的读写操作。如果单例是一个 map 或结构体字段,多个 goroutine 同时读写它,仍需额外同步(如 sync.RWMutex)。
例如:
type Cache struct {
data map[string]string
mu sync.RWMutex
}
func (c *Cache) Set(k, v string) {
c.mu.Lock()
c.data[k] = v
c.mu.Unlock()
}
func (c *Cache) Get(k string) string {
c.mu.RLock()
defer c.mu.RUnlock()
return c.data[k]
}
漏掉锁是高频问题——开发者以为“单例=线程安全”,其实只是“创建唯一”,内部状态是否并发安全,得看具体实现。尤其要注意切片、map、通道这些非原子类型。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











