怎么使用 sync.Once 确保并发初始化只执行一次?

文章导读
使用包级变量声明 sync.Once 实例,并在初始化逻辑中调用 Do 方法,可确保并发环境下代码只执行一次。适用场景包括单例模式、配置加载,主要风险是 Do 内 panic 会导致后续调用静默失败且无法重试。
📋 目录
  1. A 快速处理思路
  2. B 为什么会这样
  3. C 分步处理
  4. D 怎么验证是否生效
  5. E 常见坑
  6. F 常见问题
  7. G 参考来源
A A

使用包级变量声明 sync.Once 实例,并在初始化逻辑中调用 Do 方法,可确保并发环境下代码只执行一次。适用场景包括单例模式、配置加载,主要风险是 Do 内 panic 会导致后续调用静默失败且无法重试。

先说结论:sync.Once 是 Go 标准库唯一官方设计的“只执行一次”并发原语,内部通过原子操作 + 互斥锁保证线程安全。

  • 适合:全局单例初始化、配置加载、资源惰性加载
  • 先看:确保 once 实例是包级变量,避免函数内声明
  • 建议:在 Do 内自行处理 panic 和错误,外部封装错误返回

快速处理思路

直接在包级别声明 sync.Once 变量,将初始化逻辑包裹在匿名函数中传入 Do 方法。

var once sync.Once
var instance *Config

func Init() {
    once.Do(func() {
        instance = loadConfig()
    })
}

为什么会这样

sync.Once 能保证只执行一次是因为内部维护了一个 uint32 状态位和互斥锁,通过原子操作判断是否已执行。

底层使用 atomic.CompareAndSwapUint32 检查状态标记,首次调用时加锁执行函数并将状态置为已完成,后续调用直接跳过同步路径。这种机制建立了 happens-before 关系,保证初始化完成前所有字段写入对其他 goroutine 可见。

分步处理

1. 声明包级变量:将 sync.Once 实例声明在包级别,确保所有 goroutine 共享同一个实例。

2. 定义初始化函数:编写无参无返回的匿名函数,将耗时逻辑、赋值操作完整写入函数内部。

3. 调用 Do 方法:在需要初始化的地方调用 once.Do,传入初始化函数。

4. 处理错误状态:由于 Do 不返回错误,需在包级变量中单独存储 error,供外部检查。

怎么验证是否生效

通过并发测试脚本同时调用初始化函数,观察日志中初始化逻辑是否仅打印一次。

怎么使用 sync.Once 确保并发初始化只执行一次?

检查全局变量状态,确认多个 goroutine 获取到的实例地址一致,且未出现竞态导致的空指针。

常见坑

1. 函数内声明实例:在函数内部或循环中声明 sync.Once 会导致每次调用都创建新实例,失去单次执行意义。

2. Panic 静默失败:Do 内函数 panic 会被标记为已完成,后续调用不再执行,且 panic 错误被吞掉。

3. 无法返回错误:Do 方法签名固定,无法直接返回 error,需自行封装全局错误变量。

4. 阻塞首次请求:若初始化耗时较长,所有并发等待的 goroutine 都会阻塞,建议提前在 main 函数触发。

常见问题

sync.Once 内 panic 会重试吗?

不会重试。一旦 Do 内函数 panic,sync.Once 会将状态标记为已完成,后续调用直接返回,不会再次执行初始化逻辑。

如何给 Do 函数传递参数?

Do 只接受无参函数,需通过闭包捕获外部变量,将参数写在闭包上下文中。

sync.Once 执行后能重置吗?

不支持重置。sync.Once 没有公开 API 重置状态,一旦执行完成,该实例无法再次用于初始化。

sync.Once 和 init 函数有什么区别?

init 在包导入时强制运行,无法懒加载;sync.Once 支持按需初始化,适合运行时动态依赖场景。

参考来源

  • Go 的 sync.Once:确保代码只执行一次的并发原语
  • Go 语言单实例函数初始化如何保证_Go 语言 sync.Once 并发安全【详解】
  • Golang 怎么用 Once 确保只执行一次_Golang 如何初始化只运行一次的代码逻辑【技巧】
  • 如何使用 Golang 的 sync.Once 确保单次执行_Golang 并发编程中确保一次执行技巧
  • Golang sync.Once 如何保证只执行一次_Golang Once 单次执行教程【干货】
  • 使用 Golang 中的 sync.Once 实现单例模式 Go 语言并发环境下安全初始化
  • 如何在 Golang 框架中利用 sync.Once 实现线程安全的单例模式初始化