Go 单次执行:sync.Once 与初始化
在并发编程中,有时需要确保某段代码在整个程序生命周期内只执行一次。比如加载配置、初始化全局资源、注册单例服务等场景。Go 语言标准库提供了 sync.Once 类型,专门用于实现这种“单次执行”逻辑。
什么是 sync.Once?
sync.Once 是一个结构体类型,它内部包含一个状态标志,保证其 Do 方法中的函数无论被多少个 goroutine 调用,都只会真正执行一次。其余调用会阻塞等待首次执行完成,然后直接返回。
它的核心优势是:
- 线程安全:无需额外加锁。
- 高效轻量:底层使用原子操作,性能开销极小。
- 语义清晰:明确表达“只做一次”的意图。
基本用法
- 声明一个
sync.Once变量(通常作为包级变量或结构体字段)。 - 在需要的地方调用
once.Do(func),传入你想只执行一次的函数。
package main
import (
"fmt"
"sync"
)
var once sync.Once
func initConfig() {
fmt.Println("正在加载配置...")
// 模拟耗时操作
}
func main() {
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go func() {
defer wg.Done()
once.Do(initConfig) // 只会打印一次 "正在加载配置..."
}()
}
wg.Wait()
}
运行结果只会输出一行 正在加载配置...,即使有 5 个 goroutine 同时调用 once.Do。
常见应用场景
场景一:延迟初始化单例对象
很多系统需要一个全局唯一的实例(如数据库连接池、日志记录器)。使用 sync.Once 可以安全地实现懒加载。
package db
import (
"database/sql"
"sync"
)
var (
dbInstance *sql.DB
once sync.Once
)
func GetDB() *sql.DB {
once.Do(func() {
// 实际连接数据库
db, err := sql.Open("mysql", "user:pass@/dbname")
if err != nil {
panic(err)
}
dbInstance = db
})
return dbInstance
}
调用 GetDB() 多次,数据库连接只会在第一次被创建,后续直接返回已有实例。
场景二:避免重复注册
某些库要求在程序启动时注册一次回调或中间件。若多个模块都尝试注册,可能导致错误。
var registerOnce sync.Once
func RegisterMiddleware() {
registerOnce.Do(func() {
http.HandleFunc("/health", healthCheckHandler)
fmt.Println("中间件已注册")
})
}
即使多个包调用 RegisterMiddleware(),路由 /health 也只会被注册一次。
场景三:一次性清理资源
虽然 sync.Once 常用于初始化,但也可用于确保清理逻辑只执行一次(例如在信号处理中)。
var cleanupOnce sync.Once
func handleSignal() {
cleanupOnce.Do(func() {
closeAllConnections()
saveStateToFile()
fmt.Println("已执行清理")
})
}
多次收到终止信号(如 SIGINT)也不会重复清理。
注意事项与陷阱
1. Do 中的函数不能 panic(除非你接受程序崩溃)
如果传给 Do 的函数发生 panic,sync.Once 会认为这次“执行已完成”,后续调用将不再重试。这意味着:
- panic 会导致初始化失败且无法恢复。
- 务必在
Do内部处理错误,不要让 panic 逃逸。
正确做法:
once.Do(func() {
db, err := sql.Open(...)
if err != nil {
log.Fatalf("初始化失败: %v", err) // 或记录错误并设置 nil,由调用方检查
}
dbInstance = db
})
2. 不要复用同一个 sync.Once 实例做多件事
每个 sync.Once 实例只对应一个单次执行任务。试图用同一个 once 控制多个不同操作会导致逻辑混乱。
❌ 错误示例:
var once sync.Once
func initA() { /* ... */ }
func initB() { /* ... */ }
// 错误:两个不同的初始化共享同一个 once
once.Do(initA)
once.Do(initB) // 这个永远不会执行!
✅ 正确做法:为每个任务创建独立的 sync.Once。
var onceA, onceB sync.Once
onceA.Do(initA)
onceB.Do(initB)
3. sync.Once 不是“只执行一次就永远成功”
它只保证函数体执行一次,不保证函数执行成功。如果函数内部因外部条件失败(如网络超时),你需要自行设计重试或降级策略——但注意,Do 本身不会再给你第二次机会。
sync.Once vs 其他方案对比
| 方案 | 是否线程安全 | 是否支持懒加载 | 是否防止重复执行 | 复杂度 |
|---|---|---|---|---|
全局变量 + init() 函数 |
是 | 否(程序启动时执行) | 是 | 低 |
sync.Mutex + 标志位 |
是 | 是 | 需手动实现 | 中 |
sync.Once |
是 | 是 | 是 | 低 |
从上表可见,sync.Once 在支持懒加载的前提下,提供了最简洁、安全的单次执行保障。
高级技巧:封装带错误处理的 Once
由于 sync.Once.Do 不返回错误,我们可以封装一个支持错误传播的版本:
type Once struct {
once sync.Once
err error
}
func (o *Once) Do(f func() error) error {
o.once.Do(func() {
o.err = f()
})
return o.err
}
使用方式:
var initOnce Once
err := initOnce.Do(func() error {
db, err := sql.Open(...)
if err != nil {
return err
}
dbInstance = db
return nil
})
if err != nil {
// 处理初始化失败
}
这样既能保证单次执行,又能获取初始化结果。
创建 sync.Once 变量
在并发环境中调用 once.Do(函数)
确保函数内部处理所有错误,避免 panic

暂无评论,快来抢沙发吧!