sync.once.do只执行一次是设计使然,因其用uint32原子变量标记状态,首次cas成功者执行函数并置为1,后续调用直接跳过;panic后状态仍为1且不重试,多goroutine首次调用仅一个执行,其余阻塞等待。

sync.Once 的 Do 方法只执行一次,这是设计使然,不是 bug,也不可绕过。它适合做「运行时按需、并发安全、仅一次」的初始化,不适合重试、热更新或跨 goroutine 协调状态。
为什么 Do 只执行一次且不重试
内部用 atomic.Bool(或旧版 uint32)标记状态:首次调用时原子地从 false 变为 true 并执行函数;一旦变为 true,后续所有调用都直接返回,不检查函数是否 panic、是否成功、是否该重试。
- 如果传入的函数 panic,
once状态仍被设为 “已完成”,后续调用永远跳过 —— 这是明确行为,不是缺陷 - 多个 goroutine 同时首次调用
Do,只有一个能进函数体,其余阻塞等待其返回(无论成功或 panic) - 没有重置接口,没有
Reset或Unmark方法,状态不可逆
Do 传函数时最常见的三个坑
编译期或运行期容易栽跟头的地方:
- 传带参闭包却没固化变量值:比如
for i := 0; i ,输出全是 <code>3—— 正确写法是val := i; once.Do(func() { fmt.Println(val) }) - 函数签名不符:
Do只接受func(),传func(x int)或func() error直接编译失败 - 在函数里启 goroutine 却没等它结束:
once.Do(func() { go heavyWork(); return })——Do只等这个函数返回,不等它 spawn 的 goroutine,业务上可能误判“初始化完成”
和 init() 到底该用谁
关键看初始化时机和依赖:
- 需要读环境变量、命令行参数、远程配置?必须用
sync.Once——init()在main()前执行,这些值还没加载 - 只是定义全局常量、注册 HTTP handler、设置日志格式?
init()更轻量、无并发顾虑、零延迟 - 初始化失败要返回错误给调用方?只能用
sync.Once+ 外层错误变量(如var initErr error),init()里 panic 会直接终止进程
真正容易被忽略的边界问题
不是“只执行一次”本身难懂,而是它和业务语义的错位:
-
sync.Once是 per-instance 的,复制即失效:放进 struct 字段(type S struct { once sync.Once })等于没用;必须是包级变量,或结构体中存指针(once *sync.Once) - 它不保护你初始化后的数据:比如
once.Do(func() { m = make(map[string]int) }),之后并发读写m仍需sync.RWMutex——Once不是sync.Map - 多个资源要协同初始化(如 DB + Cache + Logger),不能拆成多个
Once;必须合并到同一个Do函数里,否则顺序和完成态无法保证
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











