文章目录

Go 单次执行:sync.Once 与初始化

发布于 2026-04-04 10:15:05 · 浏览 3 次 · 评论 0 条

Go 单次执行:sync.Once 与初始化

在并发编程中,有时需要确保某段代码在整个程序生命周期内只执行一次。比如加载配置、初始化全局资源、注册单例服务等场景。Go 语言标准库提供了 sync.Once 类型,专门用于实现这种“单次执行”逻辑。


什么是 sync.Once?

sync.Once 是一个结构体类型,它内部包含一个状态标志,保证其 Do 方法中的函数无论被多少个 goroutine 调用,都只会真正执行一次。其余调用会阻塞等待首次执行完成,然后直接返回。

它的核心优势是:

  • 线程安全:无需额外加锁。
  • 高效轻量:底层使用原子操作,性能开销极小。
  • 语义清晰:明确表达“只做一次”的意图。

基本用法

  1. 声明一个 sync.Once 变量(通常作为包级变量或结构体字段)。
  2. 在需要的地方调用 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

评论 (0)

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

扫一扫,手机查看

扫描上方二维码,在手机上查看本文