Go Once

Go Once

一、介绍

Once 常常用来初始化单例资源 ,或者并发访问只需初始化一次的共享资源,或者在测试的时候初始化一次测试资源。

二、结构体

go 复制代码
type Once struct {
	_ noCopy 

	// done indicates whether the action has been performed.
	// It is first in the struct because it is used in the hot path.
	// The hot path is inlined at every call site.
	// Placing done first allows more compact instructions on some architectures (amd64/386),
	// and fewer instructions (to calculate offset) on other architectures.
	done atomic.Bool
	m    Mutex
}
  • noCopy : 编译期检查,避免值拷贝导致的状态失效,无内存开销
  • done : 标记函数是否已执行(核心状态)
    atomic.Bool 类型(原子布尔型 ): 提供了原子的 Load()/Store() 方法,保证并发读写的安全性,且无需加锁
  • m : 保证并发下仅执行一次的互斥锁

三、核心方法

3.1 Do(f func())

go 复制代码
func (o *Once) Do(f func()) {
	// Note: Here is an incorrect implementation of Do:
	//
	//	if o.done.CompareAndSwap(0, 1) {
	//		f()
	//	}
	//
	// Do guarantees that when it returns, f has finished.
	// This implementation would not implement that guarantee:
	// given two simultaneous calls, the winner of the cas would
	// call f, and the second would return immediately, without
	// waiting for the first's call to f to complete.
	// This is why the slow path falls back to a mutex, and why
	// the o.done.Store must be delayed until after f returns.
	// 快速路径:原子读取 done,无锁,开销极小
	if !o.done.Load() {
		// Outlined slow-path to allow inlining of the fast-path.
		 // 慢路径:加锁,保证只有一个 goroutine 执行 f
		o.doSlow(f)
	}
}

func (o *Once) doSlow(f func()) {
    // 加锁
	o.m.Lock()
	defer o.m.Unlock()
	// 双重检查:避免加锁期间其他 goroutine 已执行完 f
	if !o.done.Load() {
	// 执行完 f 后,原子设置 done 为 true
		defer o.done.Store(true)
		f()
	}
}

逻辑:

  1. 所有 goroutine 先读取 done(原子操作,无锁),若为 true 直接返回
  2. 若 done 为 false,竞争 m 锁,只有一个 goroutine 能拿到
  3. 拿到锁的 goroutine 再次检查 done(防止等待锁期间其他 goroutine 已执行),执行 f 后将 done 置为 true
  4. 其他等待锁的 goroutine 拿到锁后,发现 done=true,直接释放锁返回

四、注意事项

1. 禁止值拷贝 Once 实例

sync.Once 有状态的结构体 (内部维护 donem),值拷贝会生成全新的实例,导致 "仅执行一次" 的语义失效

2. Do(f) 中 f 发生 panic 也会标记为已执行

Once 的设计逻辑是:只要进入 f() 的执行流程(哪怕 panic),done 就会被置为 true,后续调用不会再执行 f

go 复制代码
package main

import (
	"fmt"
	"sync"
)

var once sync.Once

func riskyFunc() {
	panic("初始化函数panic了") // 执行panic
}

func main() {
	// 第一次调用:执行riskyFunc,触发panic,但done会被置为true
	defer func() {
		if err := recover(); err != nil {
			fmt.Println("捕获panic:", err)
		}
	}()
	once.Do(riskyFunc)

	// 第二次调用:done已为true,不会执行riskyFunc
	fmt.Println("第二次调用Do:")
	once.Do(riskyFunc)
	fmt.Println("执行完成")
}

3. 一个 Once 实例只能绑定一个函数,不能复用

Once 是 "执行一次操作" 的语义,而非 "对每个函数执行一次"。一个 Once 实例调用 Do(f1) 后,再调用 Do(f2),f2 永远不会执行。

go 复制代码
package main

import (
	"fmt"
	"sync"
)

var once sync.Once

func f1() { fmt.Println("执行f1") }
func f2() { fmt.Println("执行f2") }

func main() {
	once.Do(f1) // f1执行,done置为true
	once.Do(f2) // done已为true,f2永远不执行
}

不同的 "一次性操作" 对应不同的 Once 实例

4. f 函数不能有参数 / 返回值(需用闭包适配)

Once.Do(f) 的参数要求是 func()(无参数、无返回值),如果需要传递参数或接收返回值,必须通过闭包实现

go 复制代码
package main

import (
	"fmt"
	"sync"
)

var once sync.Once
var result int

// 带参数的函数
func calc(a, b int) int {
	return a + b
}

func main() {
	once.Do(func() {
		// 闭包内调用带参数的函数,并接收返回值
		result = calc(10, 20)
	})
	fmt.Println("计算结果:", result) // 输出 30
}

5. 避免 Once 依赖循环或初始化顺序问题

如果 f() 内部又调用了同一个 Once 的 Do(),会导致死锁(因为 Once 的锁是不可重入的)

go 复制代码
package main

import (
	"fmt"
	"sync"
)

var once sync.Once

func f() {
	once.Do(g) // 内部再次调用同一个Once的Do,导致死锁
}

func g() { fmt.Println("执行g") }

func main() {
	once.Do(f)
}

五、使用

  1. Once 适合 "延迟初始化 ",而非 "懒加载所有资源 "
    Once 的性能很高,但如果把所有初始化逻辑都塞到一个 f() 里,会导致首次调用的延迟过高。建议按模块拆分 Once 实例,按需初始化
  2. 不要手动修改 Once 的内部字段
    Once 的 done 和 m 是未导出字段,手动通过反射修改会破坏其并发安全性,属于典型的 "黑客行为",绝对禁止。
  3. Once 与 init() 函数的区别
  • init():程序启动时执行(包级),无法控制执行时机,且一定会执行
  • Once.Do(f):首次调用时执行,可控制时机,且仅执行一次;
    场景选择:如果是包级的基础初始化,用 init();如果是按需初始化(比如首次使用数据库连接时),用 Once
相关推荐
hongtianzai2 小时前
Go vs Java:终极性能对决
java·开发语言·golang
贺小涛2 小时前
Golang Gin框架核心原理与架构解析
架构·golang·gin
汤姆yu2 小时前
基于python大数据的天气可视化及预测系统
大数据·开发语言·python
转角羊儿2 小时前
精灵图案例
开发语言·前端·javascript
l1t2 小时前
Qwen 3.5plus编写的求解欧拉计划901题python程序优化
开发语言·python
T0uken2 小时前
【Python】docxnote:优雅的 Word 批注
开发语言·python·word
9稳2 小时前
基于智能巡检机器人与PLC系统联动控制设计
开发语言·网络·数据库·嵌入式硬件·plc
承渊政道2 小时前
C++学习之旅【IO库相关内容介绍】
c语言·开发语言·c++·学习·macos·visual studio
Ronin3052 小时前
【Qt窗口】Qt窗口
开发语言·qt·qt窗口