Go 怎样用 atomic.Bool 实现一次开关
来源:17golang原创
时间:2026-10-17 17:32:21 448浏览 收藏
如果一个初始化动作、迁移动作或首次注册动作只能成功一次,可以把状态放进 atomic.Bool,让所有调用共享同一个开关,再用 CompareAndSwap(false, true) 抢占唯一的成功机会。返回 true 的调用继续执行后续动作,其他调用直接结束;这个判断和状态修改是一个原子操作,不会因为并发读写而产生两个“首次成功者”。
这里的“一次”指从 false 到 true 的一次条件转换。如果业务允许重新打开,就需要明确调用 Store(false) 的时机;如果状态还要和订单、锁定时间等字段保持整体一致,单独一个布尔原子值并不能替代事务或更大范围的同步。
先把一次开关的状态说清楚
atomic.Bool 是 sync/atomic 提供的类型化原子布尔值,零值为 false,常用方法包括 Load、Store、Swap 和 CompareAndSwap。它适合表示一个可以独立读取、独立改变的状态,例如“是否已经完成初始化”“是否已经发送首个通知”。
我在这种场景里通常先把对象身份固定下来:并发调用拿到的是同一个 *OnceSwitch,而不是每次函数调用新建一个局部变量。只有共享同一个原子字段,竞争才会发生在同一个状态上。

用 CompareAndSwap 让并发调用只成功一次
一次性动作最适合使用 CompareAndSwap:只有当当前值仍是 false 时,才把它替换成 true,并返回是否替换成功。多个 goroutine 同时尝试时,只有一个调用能满足这个条件;其他调用看到旧值已经不是 false,就会返回 false。
package main
import (
"fmt"
"sync"
"sync/atomic"
)
// OnceSwitch 把一次性状态集中到一个不可复制的共享对象中。
type OnceSwitch struct {
started atomic.Bool
}
// TryStart 只让第一个调用者取得后续动作的执行权。
func (s *OnceSwitch) TryStart() bool {
// 比较和写入在同一个原子操作内完成,避免两个调用都成功。
return s.started.CompareAndSwap(false, true)
}
func main() {
switcher := new(OnceSwitch) // 使用同一个指针,所有调用竞争同一状态。
var wg sync.WaitGroup
const callers = 4
wg.Add(callers)
for i := 0; i
这个示例的关键不是 goroutine 数量,而是 switcher 只创建一次,并把指针交给所有调用者。预期只有一行 winner,但具体是哪一个编号并不固定。这里的输出是根据代码逻辑说明的预期,不是本文制作时执行得到的运行截图。
如果一次性动作本身可能失败,要先决定状态表示的是“已经抢到执行权”还是“动作已经成功完成”。上面的写法表示前者:一旦某个调用把状态改成 true,后续调用就不会再进入动作。若业务要求失败后允许重试,就不能在失败时无条件保持这个状态,应重新设计回滚或状态机边界。
Load、Store 和 Swap 分别解决什么问题
Load 只读取当前布尔值,适合做展示、采样或在不需要抢占时判断状态。它不会改变开关,也不能把“先读取再执行”自动变成一次性竞争。
// IsStarted 返回一次开关在某个读取时刻的快照。
func (s *OnceSwitch) IsStarted() bool {
return s.started.Load() // 原子读取,不承担后续动作的排他性。
}
Store 是无条件写入。初始化时可以用它明确设置状态,测试或明确的生命周期切换也可以用它重置;但 Store(true) 不会告诉你之前是谁改的,Store(false) 也会重新打开一次性入口。
// Reset 只有在业务确认允许重新开始时才调用。
func (s *OnceSwitch) Reset() {
s.started.Store(false) // 重置后下一次 CompareAndSwap 可能再次成功。
}
Swap 会原子地写入新值并返回旧值。如果你需要知道“切换前是否已经打开”,它比单独 Load 再 Store 更合适,因为读取旧值和写入新值不会被另一个调用插入。
// OpenAndReport 打开开关,并报告这次写入前的旧状态。
func (s *OnceSwitch) OpenAndReport() (wasOpen bool) {
return s.started.Swap(true) // 返回旧值,false 表示此前处于关闭状态。
}
不要把 Load 加 Store 当成一次开关
下面这种写法看起来直观,但在并发下不成立:
// wrongTryStart 把判断和写入拆开,不能保证只有一个成功者。
func (s *OnceSwitch) wrongTryStart() bool {
if s.started.Load() { // 两个调用都可能同时读到 false。
return false
}
s.started.Store(true) // 另一个调用可能已经在这里之前完成了同样的写入。
return true
}
每一次 Load 和 Store 本身都可以是原子的,但两次调用之间的业务判断不是原子的。两个调用都读到 false 后,就可能都返回 true。要竞争“旧值是 false 的那个机会”,必须直接使用 CompareAndSwap(false, true)。

对象不能复制,业务动作也不能只靠一个字段
原子类型使用后不要复制。不要把包含 atomic.Bool 的结构体按值传递、按值返回或用赋值制造第二份对象;需要共享时传递指针,需要对外提供结果时读取普通的 bool 快照。复制对象会让调用者误以为自己仍在竞争同一个开关。
还要把“状态更新”和“业务动作完成”分开。CompareAndSwap 只能保证布尔字段的这次条件替换;它不会自动把数据库写入、文件创建、日志记录等多个动作包成事务。如果动作需要和其他字段保持一致,就用事务、互斥锁或明确的状态机覆盖整个一致性范围。
最后,原子性也不等于执行顺序已经满足业务要求。成功取得一次开关后,调用者仍要负责错误处理、资源清理和幂等设计;如果动作失败后必须重试,就不要把“抢到执行权”直接当成“业务已完成”。
怎么选择这四个方法
| 方法 | 适合的任务 | 要注意的边界 |
|---|---|---|
Load | 读取当前快照 | 不能和后续动作组成一次性竞争 |
Store | 无条件设置或明确重置 | 会覆盖当前状态,重置会重新开放入口 |
Swap | 写入新值并拿到旧值 | 表示替换,不表达复杂业务条件 |
CompareAndSwap | 只有旧值匹配时才更新 | 最适合争抢唯一的首次执行权 |
实际使用时可以按一个问题判断:你需要的是“看一眼状态”、“直接覆盖状态”、“知道替换前是什么”,还是“只有状态仍为某值才允许我改写”?一次开关通常属于最后一种,因此从 CompareAndSwap(false, true) 开始设计最清楚。
官方文档入口
官方地址:https://pkg.go.dev/sync/atomic
版本说明:https://go.dev/doc/go1.19
内存模型:https://go.dev/ref/mem
相关问题
atomic.Bool 和互斥锁怎么选?只保护一个独立布尔状态时原子类型更直接;需要同时保护多个字段或一段临界区时,互斥锁通常更容易表达整体不变量。
为什么不能先 Load 再 Store?因为判断和写入之间存在竞争窗口,多个调用都可能基于旧的 false 返回成功。
一次性动作失败后能不能重试?可以,但要把“已抢到执行权”和“动作已完成”拆开,不能无条件把失败当作成功状态。
Store(false) 会发生什么?它会重新打开开关,之后另一个调用可能再次通过 CompareAndSwap(false, true)。
-
860 收藏
-
843 收藏
-
826 收藏
-
809 收藏
-
792 收藏
-
403 收藏
-
209 收藏
-
313 收藏
-
468 收藏
-
460 收藏
-
219 收藏
-
408 收藏
-
376 收藏
-
495 收藏
-
412 收藏
-
309 收藏
-
127 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习