推荐用 sync.once 实现单例,需配合包级变量和显式错误处理;仅用 once.do(func(){instance=&service{}}) 会导致初始化失败时 instance 为 nil 引发 panic、无法捕获错误、每次调用新建 once 失去唯一性。

推荐用 sync.Once,但它本身不是单例,只是“只执行一次”的保障工具;真正优雅的单例,是它 + 包级变量 + 显式错误处理的组合。
为什么不能只写 once.Do(func() { instance = &Service{} })
这行代码看似简洁,但藏着三个实际问题:
- 如果
&Service{}初始化失败(比如依赖的 DB 连接超时),instance会是nil,后续调用直接 panic ——sync.Once.Do不捕获 panic,也不返回 error -
instance是包级变量,类型必须明确(如*Service),不能靠返回值推导;若初始化函数返回(*Service, error),你得自己存 error 状态 - 把
sync.Once声明在函数内部(比如放在GetInstance()里)就完全失效:每次调用都新建一个once,失去“全局唯一执行”的意义
sync.Once 必须搭配包级变量和错误返回才可靠
标准写法要同时声明 sync.Once、目标实例变量、错误变量,三者同级且包可见:
var (
serviceOnce sync.Once
service *Service
serviceErr error
)
<p>func GetService() (<em>Service, error) {
serviceOnce.Do(func() {
service, serviceErr = NewService() // NewService 返回 (</em>Service, error)
})
return service, serviceErr
}
</p>
关键点:
-
service和serviceErr必须是同级包变量,类型一致(不能一个是指针、一个是值) -
GetService()每次都返回当前状态,调用方能显式判断err != nil,而不是静默容忍nil指针 - 多次调用
GetService()不会重复初始化,但也不会掩盖首次失败——这是设计使然,不是 bug
结构体字段嵌入 sync.Once 适合有状态客户端
当单例逻辑属于某个具体类型(比如带连接池的 HTTP 客户端),把 sync.Once 作为结构体字段更清晰:
type Client struct {
once sync.Once
client *http.Client
mu sync.RWMutex
}
<p>func (c <em>Client) Do(req </em>http.Request) (*http.Response, error) {
c.once.Do(c.initClient) // 注意:必须是指针接收者,否则副本里的 once 不生效
c.mu.RLock()
defer c.mu.RUnlock()
return c.client.Do(req)
}</p><p>func (c <em>Client) initClient() {
c.client = &http.Client{Timeout: 30 </em> time.Second}
}
</p>
注意:
- 方法接收者必须是
*Client,否则c.once操作的是临时副本,无法跨调用持久化状态 - 这种模式天然支持多实例隔离(比如不同配置的
Client),比全包级变量更易测试和复用 - 别在
initClient里做阻塞操作(如长耗时 DNS 查询),否则所有 goroutine 会卡在Do()等待
别用 init() 替代 sync.Once 加载外部资源
init() 只适合无副作用、必成功的小初始化(如注册 JSON tag 映射)。以下全是危险操作:
- 在
init()里读取环境变量并连接数据库 —— 失败直接导致整个进程启动失败,日志难追溯 - 用
init()加载配置文件 —— 测试时无法 mock 路径或内容,也无法重试 - 依赖导入顺序做初始化排序 —— Go 不保证多个
init()的执行顺序,仅按包依赖图拓扑排序,极易出错
真正需要延迟、可重试、可测试的初始化,sync.Once 是目前 Go 生态最稳的选择。它的边界很清晰:不负责对象生命周期,不处理并发访问控制,只守好“第一次”这道门 —— 其余责任,得由你来补全。











