首页 / Go 语言入门教程 / sync.Map 与 atomic

Go 语言入门教程

sync.Map 与 atomic

本教程共 80 篇 · 第 64 篇 · 更新于 2026-07-27 · 约 8 分钟阅读

GoGo 入门教程sync.Mapatomic原子操作CAS并发安全无锁

64. sync.Map 与 atomic

本节目标:学会用 sync.Map 做并发安全的 map,掌握 sync/atomic 包的原子类型、CAS 操作和 atomic.Value

普通 map 不并发安全

内置 map 并发读写会直接 fatal:

m := make(map[int]int)
for i := 0; i < 10; i++ {
	go func() {
		m[1] = 1 // fatal error: concurrent map writes
	}()
}

解决办法有两种:加锁,或用 sync.Map

sync.Map

sync.Map 是开箱即用的并发安全 map,不用初始化:

package main

import (
	"fmt"
	"sync"
)

func main() {
	var m sync.Map

	m.Store("a", 1)          // 存
	m.Store("b", 2)

	v, ok := m.Load("a")     // 取,返回值和是否存在
	fmt.Println(v, ok)       // 1 true

	m.Delete("a")            // 删

	// 存在就返回旧值,不存在就存入
	val, loaded := m.LoadOrStore("b", 3)
	fmt.Println(val, loaded) // 2 true

	// 删除并返回旧值
	val, loaded = m.LoadAndDelete("b")
	fmt.Println(val, loaded) // 2 true

	// 遍历
	m.Store("c", 3)
	m.Range(func(k, v any) bool {
		fmt.Println(k, v)
		return true // 返回 false 可提前退出
	})
}

什么时候用 sync.Map

sync.Mapmap + Mutex 慢不少,但某些场景有优势:

  • key 写入一次后反复读(配置缓存)
  • 多个 goroutine 读写不同的 key(没有热点的 key)

如果是高频更新所有 key,map + RWMutex 通常更快。

Warning

sync.Map 的 key 和 value 都是 any 类型,丢失了类型安全。取值后要类型断言:

v, _ := m.Load("count")
count := v.(int) // 断言成 int

atomic 包

sync/atomic 提供原子操作,用于简单数值的并发安全读写,比锁轻量。

原子操作是不可分割的,执行过程中不会被其他 goroutine 打断。适合计数器、标志位这类场景。

原子类型(Go 1.19+)

Go 1.19 起推荐用原子类型,比函数式 API 更直观:

var count atomic.Int64

func main() {
	count.Store(0)         // 存
	count.Add(1)           // 加
	count.Add(5)
	fmt.Println(count.Load()) // 6

	old := count.Swap(10)  // 交换,返回旧值
	fmt.Println(old, count.Load()) // 6 10
}

支持的原子类型:

  • atomic.Bool
  • atomic.Int32 / atomic.Int64
  • atomic.Uint32 / atomic.Uint64
  • atomic.Uintptr
  • atomic.Pointer[T](泛型,1.19+)
  • atomic.Value(存任意类型)

每个类型都有 LoadStoreSwap 方法,整型还有 Add

函数式 API

也可以用函数形式,操作普通变量:

var count int64

atomic.StoreInt64(&count, 0)
atomic.AddInt64(&count, 1)
fmt.Println(atomic.LoadInt64(&count))

新版推荐用原子类型,函数式 API 主要用于改老代码或操作结构体字段。

CAS 比较并交换

CAS(Compare-And-Swap)是无锁编程的核心。它比较内存值和期望值,相同才更新:

var count atomic.Int64

func add(n int64) {
	for {
		old := count.Load()
		new := old + n
		if count.CompareAndSwap(old, new) {
			break // 成功
		}
		// 失败说明被别人改了,重试
	}
}

CAS 的逻辑:读旧值 -> 算新值 -> 比较交换。如果期间没人改过,成功;否则重试。这就是乐观锁。

Note

CAS 适合「竞争不激烈、操作简单」的场景。竞争太激烈会频繁重试,反而比锁慢。另外 CAS 解决不了 ABA 问题(值从 A 变 B 又变 A),需要加版本号。

atomic.Value

atomic.Value 能存任意类型的值,用于「配置热更新」这类场景:

var config atomic.Value

func main() {
	config.Store(&Config{timeout: 10})

	go func() {
		cfg := config.Load().(*Config) // 取出后类型断言
		fmt.Println(cfg.timeout)
	}()

	// 热更新配置
	config.Store(&Config{timeout: 30})
}
Warning

atomic.Value 有两个限制:

  1. 不能存 nil
  2. 前后存的值类型必须一致,否则 panic

原子 vs 互斥锁

特性atomicMutex
适用场景简单数值读写复杂临界区
性能高,无阻塞有阻塞和调度开销
操作粒度单个变量一段代码
能否保护多变量不能
Tip

简单计数器用 atomic.Int64,保护多个关联变量用 Mutex。别用原子操作去模拟复杂逻辑,容易出错。

原子类型不能复制

和锁一样,原子类型包含内部状态,复制后就不是同一个了:

// 错误
func bad(c atomic.Int64) {
	c.Add(1) // 改的是副本
}

// 正确
func good(c *atomic.Int64) {
	c.Add(1)
}

小结

  • sync.Map:并发安全 map,适合读多写少、key 不热点
  • key/value 是 any,取值要类型断言
  • atomic 原子类型:Int64BoolPointer[T] 等,比锁轻量
  • CAS 比较交换实现乐观锁,竞争不激烈时高效
  • atomic.Value 存任意类型,用于配置热更新,类型须一致
  • 原子类型和锁都不能复制,用指针传递

下一节进入 context 包,它是控制 goroutine 生命周期的核心工具。