sync.once 仅保证函数首次执行且不阻塞后续调用,若需所有并发调用等待首次执行完成,须结合 sync.mutex、sync.waitgroup 或 channel 实现串行化或信号通知,并添加 context 超时控制与错误处理。

用 sync.Once 确保函数只执行一次,但注意它不阻塞后续调用
sync.Once 是 Go 标准库提供的最直接方案,但它只保证「首次调用执行,其余调用直接返回」,不提供排队或等待机制。如果你的需求是「让后续调用等前面那个跑完再执行」,sync.Once 不够用。
常见误用场景:以为 sync.Once.Do() 会串行化所有调用,结果发现并发调用时其他 goroutine 直接跳过函数体,没等也没报错。
-
sync.Once内部用原子操作 + mutex 实现,轻量且安全,适合初始化类逻辑(如加载配置、创建单例) - 一旦
Do()返回,无论函数是否 panic,该Once都标记为已完成,后续调用不再执行 - 不能重置,也不能判断是否已执行——只能靠外部变量配合
sync.Once记录状态
用 sync.Mutex + 标志位实现「真正的一次性执行 + 后续等待」
如果必须让所有并发调用都等第一个执行完才继续(即串行化而非跳过),就得自己加锁控制。核心思路是:用互斥锁保护执行入口 + 布尔标志记录是否已执行。
典型错误是只锁标志位检查,却不锁整个执行过程,导致多个 goroutine 同时通过检查后一起进函数体。
- 必须在锁内完成「检查 + 执行 + 标记」三步,否则竞态依然存在
- 示例结构:
var ( mu sync.Mutex done bool onceMu sync.Mutex // 如果还要支持重置,可额外加锁 ) func runOnce() { mu.Lock() defer mu.Unlock() if done { return } // 执行你的函数逻辑 yourFunc() done = true } - 注意
yourFunc()若耗时长,会阻塞所有后续调用——这是设计使然,不是 bug
用 sync.WaitGroup + sync.Once 组合实现「首次执行完毕后广播,其余调用等待」
当函数执行时间不可控,又不想让所有 goroutine 在锁里干等(比如要避免持有锁太久),可以用信号机制解耦执行与等待。
本质是把「执行」和「通知」分离:一个 goroutine 负责真正干活并结束时发信号,其他 goroutine 只负责等待信号。
- 初始化时声明:
var once sync.Once和var wg sync.WaitGroup,再用sync.Once启动唯一执行者 - 执行函数里先
wg.Add(1),结束后wg.Done();等待方调用wg.Wait() - 关键点:必须确保
once.Do()中启动的 goroutine 真正触发了wg.Done(),否则所有等待方永久阻塞 - 错误写法:
once.Do(func() { wg.Add(1); go func(){ yourFunc(); wg.Done() }() })—— 这里wg.Add(1)和 goroutine 启动不在同一上下文,可能漏调Done()
别忽略 context 和超时控制
真实服务中,没人愿意无限等一个可能卡死的初始化函数。所有带等待的方案都应支持 cancel 或 timeout。
sync.WaitGroup 本身不支持中断,sync.Once 更不支持——你得自己封装。
- 推荐组合:
sync.Once+chan struct{}+selectwithcontext.WithTimeout - 执行方写入完成 channel;等待方用
select等 channel 或 ctx.Done() - channel 类型必须是
chan struct{}(零内存开销),别用chan bool或带缓冲的 channel - 记得关闭 channel 或用
close()显式通知,否则select永远不会从该分支退出
最易被忽略的是:没有统一清理机制。比如函数执行失败后,后续调用仍会不断尝试——除非你额外设计重试或失败标记逻辑。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











