sync.Once.Do 必须声明为包级变量或指针接收者结构体字段,否则每次调用新建实例导致重复执行;初始化失败需用额外变量记录 error 并对外暴露;不可在 init() 中依赖外部资源;闭包内避免调用其他单例以防循环依赖。

sync.Once.Do 为什么不能放函数里声明
因为每次调用都会新建一个 sync.Once 实例,done 字段永远是 0,所有 goroutine 都会各自执行闭包——完全失去“只执行一次”的意义。
必须声明为包级变量(或结构体字段),且和单例实例变量作用域一致。常见错误写法:
-
func GetDB() *sql.DB { once := sync.Once{}; once.Do(...) }—— 每次都新once -
type Client struct { once sync.Once }+ 值接收者方法 ——once在副本上操作,不生效
初始化失败时 error 怎么暴露出来
sync.Once.Do 的函数签名是 func(f func()),不支持返回值,更不处理 panic。一旦初始化失败(比如 DB 连接超时),instance 保持零值,后续调用静默返回 nil,极易引发下游 panic。
正确做法是用两个包级变量同步记录状态:
立即学习“go语言免费学习笔记(深入)”;
- 声明
var serviceInst struct{ instance *Service; err error },避免instance和err不同步更新 -
once.Do闭包内调用带 error 的构造函数,完整赋值:serviceInst = struct{...}{instance: s, err: err} - 对外函数签名必须是
func GetService() (*Service, error),调用方必须检查err != nil
为什么不能在 init() 里做依赖外部资源的初始化
init() 在包导入时强制执行,无法延迟、无法传参、无法返回 error。如果这时去连数据库或读配置文件,失败会导致整个程序启动崩溃,堆栈指向模糊,难定位。
适用场景仅限无副作用、确定成功的小操作:
- 注册 encoder/decoder(如
json.Register...) - 预设常量映射表(如
map[string]int{"a":1,"b":2}) - 不能用于:加载 YAML 配置、建立 HTTP 客户端、初始化 Redis 连接池
嵌入结构体时 sync.Once 字段怎么用才安全
当单例逻辑属于某个结构体(比如一个带缓存的客户端),把 sync.Once 作为字段嵌入比全放包级变量更清晰,但必须注意接收者类型。
结构体方法必须用指针接收者,否则每次调用都是副本,once 字段不共享:
- ❌
func (c Client) GetConn() net.Conn——c.once是副本,无效 - ✅
func (c *Client) GetConn() net.Conn——c.once被所有调用共享 - 初始化闭包里不要调用其他单例的
GetXXX(),否则容易循环依赖死锁
sync.Once,而是初始化逻辑本身是否可重试、是否需超时、是否允许部分失败——这些它一概不管,得靠你手动兜底。


















