Go 的 sync/atomic 包仅适用于基础类型(int32/int64/uint32/uintptr/*T/unsafe.Pointer)的简单操作,bool/float 需转换,复合类型须用 atomic.Value;其默认 relaxed 内存序不保证顺序,强同步需 Add 或 CAS;多数场景 mutex 更安全清晰。

Go 的 sync/atomic 包不是“替代 mutex 的万能方案”,而是为极少数特定场景设计的底层工具:仅当操作对象是基础类型(int32、int64、uint32、uintptr、*T、unsafe.Pointer)且逻辑足够简单时,才适合用原子操作。滥用反而会掩盖竞态、降低可读性,甚至因内存序误用引发隐蔽 bug。
哪些变量能用 atomic 安全修改?
只能对以下类型的变量直接调用 atomic 函数:
-
int32、int64、uint32、uint64、uintptr -
bool不行 —— 必须用int32模拟(0/1),再用atomic.LoadInt32/atomic.StoreInt32 -
float32/float64不行 —— 需转成uint32/uint64后用atomic.LoadUint32等,且需注意 IEEE 754 表示与平台字节序 - 结构体、切片、map、interface{} 等复合类型不能直接原子操作 —— 若需“原子更新整个结构”,应改用
atomic.Value(它内部用读写锁+指针替换实现)
atomic.LoadUint64 和 atomic.StoreUint64 的典型误用
看似安全的读写组合,在无同步约束下仍可能违反期望顺序。例如:
var counter uint64 // goroutine A: atomic.StoreUint64(&counter, 1) // goroutine B: v := atomic.LoadUint64(&counter) // 可能读到 0(如果 Store 还没完成可见)
这不是函数 bug,而是内存模型决定的:Go 的 atomic 默认提供“relaxed”内存序,不保证与其他内存访问的顺序。若需强顺序(如“写完立刻让所有 goroutine 看到”),必须配合 atomic.AddUint64 或显式使用 atomic.CompareAndSwapUint64 构建同步点。
立即学习“go语言免费学习笔记(深入)”;
- 计数器累加首选
atomic.AddUint64(&counter, 1),它天然带 acquire/release 语义 - 标志位切换(如“启动完成”)建议用
atomic.CompareAndSwapInt32实现一次性设置,避免重复写 - 单纯读取配置标志(如
debugMode)可用atomic.LoadInt32,但写入端必须用对应 Store 或 CAS
用 atomic.Value 安全替换复杂对象
当需要原子地更新一个 map[string]int 或自定义结构体指针时,atomic.Value 是唯一标准方式:
var config atomic.Value
config.Store(&Config{Timeout: 30, Retries: 3})
// 读取
c := config.Load().(*Config)
fmt.Println(c.Timeout)
// 更新(注意:Store 接收 interface{},必须传指针)
config.Store(&Config{Timeout: 60, Retries: 5})
-
Store和Load是完全线程安全的,但要求每次Store的值类型必须一致(比如始终是*Config) - 不能对
Load()返回的值做并发写 ——atomic.Value只保证“指针替换”原子,不保护其指向内容 - 性能略低于原生原子操作(因涉及接口值拷贝和锁),但在需要替换整个对象的场景下无可替代
真正难的从来不是调用哪个 atomic 函数,而是判断“这里到底需不需要原子操作”——多数时候,一个 sync.Mutex 更清晰、更不易出错;只有当你在高频路径上操作单个整数、且 profile 明确显示锁成了瓶颈时,才值得引入 atomic 并仔细推演内存序。











