Go 语言并发编程的 “工具箱”
sync 包是 Go 语言并发编程的 “工具箱”,里面的每一个 API 都是为了解决特定的并发问题设计的。
基础协作:WaitGroup(等待组)
作用:等待一组 goroutine 全部执行完毕,是最常用的 “并发等待” 工具。
原理:内部是一个计数器,Add(n) 增加计数,Done() 减少计数,Wait() 阻塞直到计数归零。
package main
import (
"fmt"
"sync"
"time"
)
func main() {
var wg sync.WaitGroup
// 启动 3 个 goroutine
for i := 1; i <= 3; i++ {
wg.Add(1) // 启动前加 1
go func(id int) {
defer wg.Done() // goroutine 结束前减 1
fmt.Printf("Goroutine %d 开始工作\n", id)
time.Sleep(1 * time.Second)
fmt.Printf("Goroutine %d 工作完成\n", id)
}(i)
}
fmt.Println("主 goroutine 等待所有子 goroutine 完成...")
wg.Wait() // 阻塞,直到计数归零
fmt.Println("所有工作完成!")
}
适用场景:
- 并发批量处理任务(如并发下载 10 个文件、并发查询 5 个数据库);
- 主 goroutine 需要等所有子 goroutine 干完活再继续执行。
互斥锁:Mutex(互斥锁)
作用:保证同一时间只有一个 goroutine 访问共享资源,防止 “竞态条件”。
原理:Lock() 加锁,Unlock() 解锁;加锁后,其他 goroutine 必须等解锁后才能加锁。
package main
import (
"fmt"
"sync"
)
var (
count int
mu sync.Mutex
)
func increment() {
mu.Lock() // 加锁:锁住共享资源 count
defer mu.Unlock() // 函数结束前解锁
count++
}
func main() {
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
increment()
}()
}
wg.Wait()
fmt.Println("最终 count 值:", count) // 不加锁会小于 1000,加锁后一定是 1000
}
适用场景:
- 多个 goroutine 同时修改同一个共享变量(如全局计数器、共享缓存);
- 任何需要 “独占访问” 共享资源的场景。
读写锁:RWMutex(读写互斥锁)
作用:Mutex 的升级版,适用于读多写少的场景 —— 读操作可以并发,写操作必须独占。
原理:
- 读锁(
RLock()/RUnlock()):多个 goroutine 可以同时加读锁(读与读不互斥); - 写锁(
Lock()/Unlock()):写锁与读锁、写锁与写锁都互斥(写操作时,不能读也不能写)。
package main
import (
"fmt"
"sync"
"time"
)
var (
data map[string]string
rwMu sync.RWMutex
)
// 读操作:加读锁,多个 goroutine 可以并发读
func readData(key string) {
rwMu.RLock() // 加读锁
defer rwMu.RUnlock() // 解读锁
fmt.Printf("读取 %s: %s\n", key, data[key])
time.Sleep(100 * time.Millisecond) // 模拟读耗时
}
// 写操作:加写锁,独占访问
func writeData(key, value string) {
rwMu.Lock() // 加写锁
defer rwMu.Unlock() // 解写锁
fmt.Printf("写入 %s: %s\n", key, value)
data[key] = value
time.Sleep(500 * time.Millisecond) // 模拟写耗时
}
func main() {
data = make(map[string]string)
var wg sync.WaitGroup
// 启动 5 个读 goroutine(可以并发)
for i := 1; i <= 5; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
readData(fmt.Sprintf("key%d", id%2)) // 调度 0 与 1
}(i)
}
// 启动 1 个写 goroutine(会阻塞所有读)
wg.Add(1)
go func() {
defer wg.Done()
writeData("key1", "value1")
}()
wg.Wait()
/*
写入 key1: value1
读取 key1: value1
读取 key0:
读取 key0:
读取 key1: value1
读取 key1: value1
写 goroutine 抢到了锁,先执行了写入 ( goroutine 启动顺序 不保证先读后写。)
写锁释放后,所有读 goroutine 才开始执行
读 goroutine 读取 key1 时,已经被写 goroutine 写成 "value1"
读 goroutine 读取 key0 时,map 中没有这个 key,所以打印空字符串
*/
}
适用场景:
- 读多写少的共享资源(如配置文件、缓存数据);
- 需要区分 “读并发” 和 “写独占” 的场景(比
Mutex性能更好)。
单次执行:Once
作用:保证某个函数只执行一次,即使在并发场景下也如此。
原理:内部用 Mutex + 原子变量实现,Do(f) 只会调用 f 一次。
package main
import (
"fmt"
"sync"
)
type Config struct {
Env string
}
var (
config *Config
once sync.Once
)
// 初始化配置:只会执行一次
func initConfig() {
fmt.Println("初始化配置(只会执行一次)")
config = &Config{Env: "dev"}
}
// 获取配置:并发安全的单例
func GetConfig() *Config {
once.Do(initConfig) // 保证 initConfig 只执行一次
return config
}
func main() {
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go func() {
defer wg.Done()
cfg := GetConfig()
fmt.Printf("获取到配置:%v\n", cfg)
}()
}
wg.Wait()
/*
初始化配置(只会执行一次)
获取到配置:&{dev}
获取到配置:&{dev}
获取到配置:&{dev}
获取到配置:&{dev}
获取到配置:&{dev}
*/
}
适用场景:
- 单例模式(如全局配置、数据库连接池初始化);
- 只需要执行一次的初始化操作(如加载配置文件、注册服务)。
并发 Map:Map
作用:并发安全的 Map,普通 Map 并发读写会 panic,sync.Map 不需要加锁就能并发读写。
核心 API:
Store(key, value):存键值对;Load(key):取键值对;Delete(key):删键值对;Range(f func(key, value interface{}) bool):遍历所有键值对。
package main
import (
"fmt"
"sync"
)
func main() {
var m sync.Map
// 1. 并发写入
var wg sync.WaitGroup
for i := 1; i <= 5; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
key := fmt.Sprintf("key%d", id)
m.Store(key, fmt.Sprintf("value%d", id))
fmt.Printf("写入 %s\n", key)
}(i)
}
wg.Wait()
// 2. 读取单个值
if val, ok := m.Load("key3"); ok {
fmt.Printf("读取 key3: %s\n", val)
}
// 3. 遍历所有键值对
fmt.Println("遍历所有键值对:")
m.Range(func(key, value interface{}) bool {
fmt.Printf("%s: %s\n", key, value)
return true // 返回 true 继续遍历,false 停止
})
}
适用场景:
- 并发缓存(如热点数据缓存、Session 存储);
- 多个 goroutine 同时读写的 Map(避免自己加锁的麻烦);
- 注意:
sync.Map适用于 “读多写少” 且 “键值对相对稳定” 的场景,写多读少的场景性能不如 “普通 Map + Mutex”。
对象池:Pool
作用:复用对象,减少内存分配和 GC(垃圾回收)压力,提升性能。
原理:内部是一个 “池子”,Get() 从池子里取对象,Put() 把用完的对象放回池子;如果池子空了,会调用 New 函数创建新对象。
package main
import (
"fmt"
"sync"
)
// 模拟一个需要频繁创建的对象(比如缓冲区)
type Buffer struct {
data []byte
}
func main() {
// 1. 定义对象池
pool := &sync.Pool{
New: func() interface{} {
fmt.Println("创建新的 Buffer 对象")
return &Buffer{data: make([]byte, 1024)}
},
}
// 2. 第一次获取:池子空了,调用 New 创建
buf1 := pool.Get().(*Buffer)
// 判断 buf1 是否为空
if buf1 == nil || buf1.data == nil {
fmt.Println("buf1 是空对象")
} else {
fmt.Println("buf1 是有效对象")
}
// buf1 是有效对象
// 3. 用完放回池子
pool.Put(buf1)
fmt.Println("buf1 放回池子")
// 4. 第二次获取:直接从池子取,不创建新对象
buf2 := pool.Get().(*Buffer)
// 判断 buf2 是否与 buf1 是同一个对象
if buf1 == buf2 {
fmt.Println("buf2 与 buf1 是同一个对象")
} else {
fmt.Println("buf2 与 buf1 不是同一个对象")
}
// buf2 与 buf1 是同一个对象
}
适用场景:
- 频繁创建和销毁的大对象(如数据库连接、HTTP 连接、大缓冲区);
- 需要减少 GC 压力的高性能场景(如高并发 Web 服务、消息队列)。
- 复用大对象(最常用)
bytes.Buffer、[]byte切片、大型的自定义Struct。解析一个巨大的 JSON 时,你需要一个临时缓存区。用完还给Pool,下一个请求接着用。 - 数据库连接的辅助(注意不是连接池本身),虽然它不能当数据库连接池用,但可以用来存放用于构建查询语句的临时字符串缓冲。
- Web 框架上下文,比如 Gin 框架。每一个 HTTP 请求进来,Gin 都会创建一个
Context对象。Gin 并没有每次都new一个,而是从sync.Pool里取。请求结束,放回池子。
条件变量:Cond
作用:协调多个 goroutine 之间的 “等待 - 通知” 逻辑,比 channel 更灵活(比如可以一次性通知所有等待的 goroutine)。
原理:
Wait():释放锁并阻塞等待,被通知后重新加锁;Signal():通知一个等待的 goroutine;Broadcast():通知所有等待的 goroutine;- 必须和
Mutex一起用。
// 生产者 - 消费者模式
package main
import (
"fmt"
"sync"
"time"
)
func main() {
var mu sync.Mutex
cond := sync.NewCond(&mu) // 用 Mutex 创建 Cond
var queue []int // 共享队列
// 生产者:往队列里放数据
producer := func() {
for i := 1; i <= 3; i++ {
mu.Lock()
queue = append(queue, i)
fmt.Printf("生产了 %d,队列长度:%d\n", i, len(queue))
cond.Signal() // 通知一个消费者
mu.Unlock()
time.Sleep(500 * time.Millisecond)
}
}
// 消费者:从队列里取数据
consumer := func(id int) {
for {
mu.Lock()
// 队列空了就等待
for len(queue) == 0 {
cond.Wait() // 释放锁并阻塞,被通知后重新加锁
}
// 取数据
item := queue[0]
queue = queue[1:]
fmt.Printf("消费者 %d 消费了 %d,队列长度:%d\n", id, item, len(queue))
mu.Unlock()
time.Sleep(1 * time.Second)
}
}
// 启动 1 个生产者,2 个消费者
go producer()
go consumer(1)
go consumer(2)
// 简单等待
time.Sleep(5 * time.Second)
}
适用场景:
- 复杂的生产者 - 消费者模式(需要灵活控制通知逻辑);
- 需要 “一次性通知所有等待 goroutine” 的场景(如配置更新后通知所有 worker 重新加载);
Cond比较复杂,能用 channel 解决的场景优先用 channel,channel 解决不了再用Cond。
总结
| API | 核心作用 | 一句话适用场景 |
|---|---|---|
WaitGroup | 等待一组 goroutine 完成 | 并发批量处理任务,等所有任务干完再继续 |
Mutex | 互斥锁,独占访问共享资源 | 多个 goroutine 同时修改同一个变量 |
RWMutex | 读写锁,读并发、写独占 | 读多写少的共享资源(如配置、缓存) |
Once | 保证函数只执行一次 | 单例模式、全局初始化 |
Map | 并发安全的 Map | 多个 goroutine 同时读写的缓存 |
Pool | 对象池,复用对象 | 频繁创建销毁的大对象,减少 GC |
Cond | 条件变量,协调等待 - 通知 | 复杂的生产者 - 消费者、需要广播通知的场景 |
选择原则
- 优先用简单的:
WaitGroup>Mutex>RWMutex>Map>Pool>Cond; - 读多写少:优先考虑
RWMutex(锁)或Map(并发 Map); - 性能优化:频繁创建大对象时用
Pool; - 复杂协调:channel 解决不了的场景再用
Cond。