Go 高级教程:其他并发工具
如果说 Goroutine 和 Channel 是 Go 并发的“常规武器”,那么
sync包里的工具就是“特种装备”。虽然不常用,但关键时刻能救命(榨干 CPU 的最后一点性能)。
除了 这里 介绍的诸多基础并发工具外,Go 标准库还提供了一些高级并发工具,下面介绍几个比较常用的。
1. 减轻 GC 压力:sync.Pool
我们在讲 GC 的时候提过,如果你频繁申请和销毁大对象(比如 HTTP Response 对象,或者大的 byte buffer),GC 会鸭梨山大。
sync.Pool 就是为了对象复用而生的。
1.1 示例代码
1package main
2
3import (
4 "fmt"
5 "sync"
6)
7
8// 定义池子
9var bufPool = sync.Pool{
10 // New 函数:当池子里没存货时,调用它创建一个新的
11 New: func() interface{} {
12 fmt.Println("Creating new buffer")
13 return make([]byte, 1024)
14 },
15}
16
17func main() {
18 // 1.Get(): 借一个对象
19 buf := bufPool.Get().([]byte)
20
21 // 用完它...
22
23 // 2. Put(): 还回去,下次给别人用
24 // 注意:还之前最好重置一下状态(比如清空)
25 bufPool.Put(buf)
26
27 // 再次 Get,就不会触发 New,而是直接复用刚才那个
28 buf2 := bufPool.Get().([]byte)
29 _ = buf2
30}
1.2 注意事项
sync.Pool 里的对象随时可能被 GC 回收!所以绝对不要用它存数据库连接、Socket 连接这种必须长久保持的资源。它只适合存“临时垃圾”。

(图片来源网络)
2. 并发安全的 Map:sync.Map
原生的 map 是并发不安全的!如果多个 Goroutine 同时读写一个 map,程序会直接 Panic 崩溃。
虽然你可以用 sync.RWMutex 加锁保护 map,但在读多写少的场景下(比如缓存配置),锁的开销还是大。
Go 1.9 推出了 sync.Map,专门解决这个问题(并发安全的 Map)。
- 优点:读多写少场景下性能极高(内部使用了空间换时间的策略,一份只读,一份可写)。
- 缺点:API 不如原生 map 好用(全是
interface{},需要断言)。
示例代码:
1package main
2
3import (
4 "fmt"
5 "sync"
6)
7
8func main() {
9 var m sync.Map
10
11 // 1. Store: 存储键值对
12 m.Store("name", "Hank")
13 m.Store("age", 18)
14
15 // 2. Load: 读取
16 val, ok := m.Load("name")
17 if ok {
18 fmt.Printf("Found name: %v\n", val)
19 }
20
21 // 3. LoadOrStore: 读写结合(原子操作)
22 // 如果 key 存在,返回 (value, true)
23 // 如果 key 不存在,存入并返回 (value, false)
24 actual, loaded := m.LoadOrStore("city", "Beijing")
25 fmt.Printf("City: %v, Loaded: %v\n", actual, loaded)
26
27 // 4. Delete: 删除
28 m.Delete("age")
29
30 // 5. Range: 遍历 (注意:无法像 for-range 那样 break,需返回 false 停止)
31 m.Range(func(key, value interface{}) bool {
32 fmt.Printf("Key: %v, Value: %v\n", key, value)
33 return true
34 })
35}
3. 极致性能:Atomic (原子操作)
锁(Mutex)是基于操作系统的,抢锁失败会发生线程上下文切换,成本较高。
如果你的并发操作仅仅是给一个整数 +1,用锁简直是杀鸡用牛刀。这时候该 CAS (Compare And Swap) 登场了。
Go 的 sync/atomic 包提供了 CPU 指令级的原子操作。
1package main
2
3import (
4 "fmt"
5 "sync"
6 "sync/atomic"
7)
8
9func main() {
10 var count int64
11 var wg sync.WaitGroup
12
13 for i := 0; i < 1000; i++ {
14 wg.Add(1)
15 go func() {
16 defer wg.Done()
17 // 原子加 1,绝对线程安全,且比 Mutex 快得多
18 atomic.AddInt64(&count, 1)
19 }()
20 }
21 wg.Wait()
22 fmt.Println(count) // 1000
23}
Atomic 操作不是靠软件层面的锁实现的,而是直接对应 CPU 的 CAS (Compare-And-Swap) 指令(如 x86 的 LOCK CMPXCHG)。 CPU 保证了在这个指令执行期间,内存总线是锁定的,其他核心无法同时修改这块内存。因此极快,且没有线程切换开销。
3.1 思考与局限
问题:Atomic 虽然快,但它只能同步一个变量。如果我要保证 User 结构体里的 Name 和 Age 字段同时被原子更新(要么都变,要么都不变),Atomic 还能搞定吗?
解答:不能。Atomic 只能保证单个整数或指针的原子性。这种跨多个字段的原子性更新,必须使用 sync.Mutex 加锁,或者使用 atomic.Value / atomic.Pointer[T] (Go 1.19+) 整体替换结构体指针(类似于 Java 的 AtomicReference)。
4. 隐形杀手:Data Race (数据竞争)
并发程序 bug 最难以调试。有时候程序跑了一整天都正常,突然半夜崩了。这通常是 数据竞争 导致的, 关于数据竞争和竞态条件,可以看 这里。
定义:两个以上的 Goroutine 同时访问同一块内存,且至少有一个在写,还没有加锁。
Go 提供了一个神器来检测它:-race。
检测实战
写一段有 Bug 的代码:
1func main() {
2 i := 0
3 go func() { i++ }() // 协程写
4 fmt.Println(i) // 主程读
5}
直接运行可能没报错。但加上 -race 运行:
1$ go run -race main.go
2
3WARNING: DATA RACE
4Read at 0x00c00001c0b8 by main goroutine:
5...
6Previous write at 0x00c00001c0b8 by goroutine 7:
7...
8Found 1 data race(s)
强烈建议:在开发和测试环境(CI/CD)中,始终开启 -race 编译你的程序。虽然会慢一点,但能救命。
5. 总结
- sync.Pool:想减轻 GC 压力,复用临时对象时用。
- sync.Map:读多写少的超高并发缓存场景用。
- atomic:简单的计数器、标志位修改,用它替代 Mutex。
- -race:一定要在测试阶段开启,它是并发 Bug 的照妖镜。