sync.Once是最安全的单例初始化方式,天然解决并发首次调用竞态问题,只执行一次闭包;须作包级或结构体字段,避免局部变量失效;panic后会持续失败,需自行兜底。

Go 里 sync.Once 是最安全的单例初始化方式
直接用 sync.Once 做初始化保护,比手写锁或检查更可靠。它天然解决多协程并发首次调用时的竞态问题,且只执行一次闭包,不会重复初始化。
常见错误是自己用 if instance == nil + sync.Mutex 判断,但没加 double-check(即锁内再判一次),导致可能初始化多次或 panic。
-
sync.Once不关心你初始化的是什么类型,只保证闭包只跑一次;适合全局配置、DB 连接池、日志实例等 - 不要把
sync.Once放在函数局部变量里——它必须是包级或结构体字段,否则每次调用都新建一个,失去意义 - 如果初始化函数可能 panic,
sync.Once会记住这次失败,后续调用仍会 panic;需自行兜底(比如用返回值+重试逻辑)
用 init() 函数实现“编译期单例”要谨慎
init() 确实只运行一次,但它发生在包加载时,无法按需延迟初始化,也不支持传参或错误处理。一旦出错,整个程序启动失败,排查困难。
典型误用场景:在 init() 里连数据库、读配置文件、启动 HTTP server——这些操作都可能失败或耗时,不该塞进 init()。
-
init()适合无副作用、确定成功的小型初始化,比如注册 encoder、预设常量映射表 - 若依赖外部资源(如文件、网络),务必改用
sync.Once+ 显式GetXXX()函数 - 多个
init()函数执行顺序由导入路径决定,不可控;别指望靠它做依赖排序
带参数的单例不能靠全局变量硬编码
需要传参(比如 DB 地址、超时时间)的单例,不能直接定义全局变量然后在 init() 或包初始化时赋值。那样会导致参数固化、无法测试、难以复用。
正确做法是把参数收进构造函数,单例逻辑藏在私有变量 + sync.Once 后面,对外暴露带参的获取函数。
- 避免写
var db *sql.DB全局变量 +func InitDB(dsn string);这破坏封装,且无法防止重复 Init - 推荐模式:
var once sync.Once+var instance *Service+func NewService(cfg Config) *Service+func GetService() *Service - 测试时可直接调用
NewService构造新实例,不污染全局状态
结构体内嵌 sync.Once 是可选但推荐的封装方式
当单例逻辑属于某个结构体(比如一个带缓存的客户端),把 sync.Once 作为字段嵌入,比全放包级变量更清晰、更易隔离。
注意:结构体方法必须是指针接收者,否则每次调用都是副本,once 字段不生效。
- 字段声明示例:
type Client struct { once sync.Once; client *http.Client } - 方法中必须用
(*Client).DoRequest这类指针接收者,否则c.once.Do(...)操作的是临时副本 - 这种写法适合“每个配置一套单例”的场景,比如不同 endpoint 的独立 HTTP 客户端
真正麻烦的不是写单例,而是判断“这个东西到底该不该做成单例”。连接池、日志器、配置管理器通常需要;而普通工具函数、DTO 结构体、纯计算逻辑,强行套单例反而增加耦合和测试成本。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











