数据竞争是未定义行为不是概率问题 — 原语全家福 · happens-before 规则 · race detector 工作流
Go 的并发安全由两层支撑: 工具层(Mutex/atomic/Once/sync.Map…)解决"怎么同步", 法律层(内存模型的 happens-before 规则)定义"什么才算同步成功"。数据竞争不是小概率 bug 而是未定义行为 — 唯一正确姿势是: 写并发代码时明确每条共享数据的 HB 边, 并让 -race 常驻 CI。
counter++ 是读-改-写三条指令, 两个 goroutine 交错后各写回自己的 +1, 总增量丢失。atomic.AddInt64 把三步合成一条 CPU 指令, 从根上消除交错。go func() { counter++ }() // 读→+1→写 三条指令 go func() { counter++ }() // → 结果可能 = 1, 丢了 1 次 atomic.AddInt64(&counter, 1) // 关键: 三步合成一条原子指令
mu.Lock(); critical(); mu.Unlock() // 正常模式: 新到者与唤醒者抢, 吞吐好 // 某 G 等待 > 1ms → 饥饿模式: FIFO 交接 (1.9+) // 关键: 严格 P99 场景尾延迟有保证, 不怕抽奖
var mu sync.RWMutex mu.RLock(); defer mu.RUnlock() traverse(bigTree) // 读临界区长(遍历)才划算 // 关键: 临界区极短或写频繁时, RWMutex 反而更慢
var m sync.Map m.Store(k, v); v, ok := m.Load(k) // 甜区1: 写少读多且键集稳定 (如配置表) // 甜区2: 多 G 读写不相交键 (如每连接缓存) // 关键: 其余场景 RWMutex+map 通常更快且类型安全
var mu sync.Mutex // 声明即用, 无须构造 mu.Lock(); x++; mu.Unlock() type S struct{ mu *sync.Mutex } // 关键: 传指针不传值 // 零值可用的代价: 拷贝即事故 (go vet: copies lock)
# go test -race ./... ← CI 必开 # 原理: 编译插桩 + 向量时钟, 无同步的并发读写即报 # 边界: 内存 5-10x / CPU 2-20x; 只报"发生了的" // 关键: 只用于测试环境, 不上生产
g, ctx := errgroup.WithContext(ctx) g.Go(func() error { return fetchA(ctx) }) g.Go(func() error { return fetchB(ctx) }) err := g.Wait() // → 任一 error: ctx 取消, 返回首个错误 // 关键: WaitGroup + 错误收集 + 联动取消 一件套
var qps atomic.Int64 // 1.19+ 泛型原子类型, 零值可用 qps.Add(1) // 中间件里每个请求 +1 _ = qps.Load() // 监控协程每秒读一次 // Mutex 版本在同一热点上慢 3-5 倍 (锁竞争 + 缓存行弹跳)
网关路由表每分钟全量重建, 读多写极少 — 读侧无锁, 写侧整体替换:
var routes atomic.Pointer[map[string]Route] func reload(newRoutes *map[string]Route) { routes.Store(newRoutes) // 一次原子替换, 读侧立刻可见 } func match(path string) Route { r := routes.Load() // 无锁读; 拿到的是不可变快照 return (*r)[path] // 快照永不修改 → 天然并发安全 }
套路核心: 写时整体替换不可变快照 — 读侧永远拿到完整一致的一代数据, 比"锁住 map 逐项改"又快又安全。
var initDB = sync.OnceValue(func() *sql.DB { // 1.21+ db, err := sql.Open("postgres", dsn) if err != nil { log.Fatal(err) } return db }) // 任意并发首次调用只有一个真正初始化, 其余阻塞等待结果 func Query(q string) { initDB().Query(q) }
缓存击穿的 Go 标准解法 — 同 key 并发只放一个穿透:
import "golang.org/x/sync/singleflight" var sf singleflight.Group func GetUser(ctx context.Context, id string) (*User, error) { if u := cache.Get(id); u != nil { return u, nil } v, err, _ := sf.Do(id, func() (any, error) { // 同 key 并发在此合并 u, err := db.GetUser(ctx, id) if err == nil { cache.Setex(id, u, 60) } return u, err }) return v.(*User), err // 所有等待者拿到同一结果 }
一把锁保一个大 map 撑不住时, 按 hash 分 N 片各锁各的:
type ShardedMap[T any] struct { shards [ 64 ]struct { mu sync.RWMutex m map[string]T } } func (s *ShardedMap[T]) Set(k string, v T) { i := fnv32(k) % 64 // 同 key 恒定落同一片 s.shards[i].mu.Lock(); defer s.shards[i].mu.Unlock() s.shards[i].m[k] = v // 64 片 = 并发度 ×64 }
背压三件套: 有界缓冲 + 非阻塞尝试 + 满载策略:
queue := make(chan Event, 10000) func submit(e Event) error { select { case queue <- e: return nil default: // 满了: 立刻降载, 不阻塞业务 dropped.Inc() return ErrBusy // 上层转同步写/丢弃/告警 } }
现代并行子任务的完整形态: 并发上限 + 错误联动一行搞定:
g, ctx := errgroup.WithContext(ctx) g.SetLimit(20) // 最多 20 个并发子任务 for _, id := range ids { g.Go(func() error { // 超 20 个会在 Go 内部排队 return syncOne(ctx, id) // 失败 → ctx 取消 → 全体快速退出 }) } err := g.Wait()
读多写少别拍脑袋 — 三行代码压测出真相:
// go test -bench Map -benchmem -cpu 4,16 func BenchmarkRWMutex(b *testing.B) { ... } // RWMutex + map func BenchmarkSyncMap(b *testing.B) { ... } // sync.Map func BenchmarkShard(b *testing.B) { ... } // 分片锁 // 常见结论: 读极多写极少 → sync.Map; 读写均衡 → 分片锁; 小数据 → 单 Mutex 反而最快
多次触发的关闭信号只生效一次, worker 全部退出后才返回:
type Pool struct { wg sync.WaitGroup stop chan struct{} once sync.Once // 保证 close 只发生一次 } func (p *Pool) Shutdown() { p.once.Do(func() { close(p.stop) }) // 广播退出, 重复调用安全 p.wg.Wait() // 等所有 worker 收尾 }
多 worker 抢任务, 用 atomic CAS 避免"检查+设置"的竞态窗口:
var state atomic.Int32 // 0=idle 1=running 2=done func claim() bool { // 恰好一个 worker 成功 for { s := state.Load() if s != 0 { return false } // 已被领走 if state.CompareAndSwap(s, 1) { // 原子"检查+占用" return true } // CAS 失败: 有人抢先, 重试看最新值 } }
go vet 会报 copies lock。正解: 用指针或嵌入 *sync.Mutex; struct 按 pointer 传递。func (s S) Inc() { s.mu.Lock(); s.n++; s.mu.Unlock() } // 错: 值接收者=每次拷贝新锁 // go vet: "Inc passes lock by value: sync.Mutex" func (s *S) Inc() { s.mu.Lock(); s.n++; s.mu.Unlock() } // 对: 指针接收者
go 语句之前执行。go func() { wg.Add(1); defer wg.Done(); t() }() // 错: Wait 可能抢先 wg.Add(1) // 对: Add 在 go 语句之前 go func(t Task) { defer wg.Done(); t() }(t)
func read() { mu.RLock(); defer mu.RUnlock(); helper() } func helper() { mu.RLock(); defer mu.RUnlock() } // 错: 重入=死锁 // 对: 拆小临界区, helper 不拿锁 (提供内部版/外部版两个入口)
var m sync.Map for i := 0; i < 1e6; i++ { m.Store(key(i), i) } // 错: 高频写反而慢 var mu sync.RWMutex; m2 := map[K]V{} // 对: 甜区外老实上锁 mu.Lock(); m2[k] = v; mu.Unlock()
if flag { ... } 的检查与动作之间仍是竞态窗口(TOCTOU)。正解: CAS 状态机 (CompareAndSwap) 或锁住整个决策过程。if !started.Load() { // 错: 检查与动作之间仍是窗口 start() // 两个 G 都可能通过检查 started.Store(true) } if started.CompareAndSwap(false, true) { // 对: 原子决策 start() // → 恰好一个 G 执行 }
# 错: -race 全绿就宣布并发安全 # 对: 必要不充分 — 补并发 review + 压测覆盖 go test -race -count=100 ./... # 多轮放大触发概率 # 预发环境混流再挂 -race (非生产层)
v := v。for _, v := range items { go func() { use(v) }() // 错(1.21-): 并发读到错值 v := v // 对: 显式遮蔽 (或升 1.22+) go func() { use(v) }() }
mu.Lock(); callback(event); mu.Unlock() // 错: 持锁调外部函数 mu.Lock(); ev := *event; mu.Unlock() // 对: 先拷贝数据 callback(ev) // 回调移出临界区
mu.Lock(); defer mu.Unlock() // 错: 中间 IO 全程持锁 resp, err := callRemote() mu.Lock(); v := cache[k]; mu.Unlock() // 对: 只锁取数据一小段 resp, err = callRemote(v)
type M struct{ min, max atomic.Int64 } // 错: 各自原子≠整体一致 m.min.Store(2); m.max.Store(9) // 读者见新 min 旧 max type M struct{ mu sync.Mutex; min, max int64 } // 对: 锁保复合不变量
var wg sync.WaitGroup // 错: 跨轮共用, Wait/Add 交错 for round := 0; round < n; round++ { wg := sync.WaitGroup{} // 对: 每轮新建 for _, t := range tasks { wg.Add(1); go work(&wg, t) } wg.Wait() }
once.Do(func() { mustInit() }) // 错: panic 后视同"已执行" // 之后 Do 直接返回 → 永久坏状态, 不再重试 // 对: f 内 recover 并暴露重试入口 once.Do(func() { if r := recover(); r != nil { allowRetry() } })
go func() { for k := range m { _ = k } }() m[k] = v // 错: → fatal error: // concurrent map iteration and map write // 对: 迭代中收集变更, 循环外统一应用 (或 sync.Map)
var s []int go func() { s = append(s, 1) }() // 错: 互相覆盖丢数据 go func() { s = append(s, 2) }() s := make([]int, n) // 对: 预分配各写各下标 for i := 0; i < n; i++ { go func(i int) { s[i] = work(i) }(i) }
buf := make([]byte, 0, 4096) // G 私有 mu.Lock(); buf = append(buf, b...); mu.Unlock() // 错: 纯开销 buf = append(buf, b...) // 对: 所有权明确无需锁 out <- buf // 交接点才需要(chan 自带)
pool.Put(userSession) // 错: 当缓存期待命中 v, _ := pool.Get().(*Session) // GC 一到池可能清空 // 对: Pool 只降分配率 (Get/Hit 率进指标) // 数据缓存用带 TTL 结构 (ristretto/bigcache)
x.Store(x.Load() + 1) // 错: 两步之间已被别的 G 改 x.Add(1) // 对: 原子加 for { // 复杂逻辑用 CAS 循环 o := x.Load() if x.CompareAndSwap(o, o*2) { break } }
ctx = context.WithValue(ctx, "order", order) // 错: 业务参数塞 ctx process(ctx, order) // 对: 显式传参 ctx = context.WithValue(ctx, traceIDKey, tid) // ctx 只带元数据
// 错: 以为死锁必然触发 "all goroutines are asleep" // 部分死锁(还有 worker 在跑)永远不报 select { // 对: 超时兜底 case ch <- v: case <-time.After(3 * time.Second): return ErrStuck // + pprof goroutine 巡检 }
# 错: 本地 -race 全绿就不管了 # 对: -race 进 CI 必跑 + 预发长时间混流 go test -race ./... GOFLAGS=-race ./app --canary # 预发金丝雀(非生产层, 接受慢)