目录
- [🟡 Go入门到精通 | 同步原语:Go并发的工具箱](#🟡 Go入门到精通 | 同步原语:Go并发的工具箱)
-
- [🔒 为什么需要同步原语](#🔒 为什么需要同步原语)
- [🔐 sync.Mutex:互斥锁](#🔐 sync.Mutex:互斥锁)
- [📖 sync.RWMutex:读写锁](#📖 sync.RWMutex:读写锁)
-
- 锁的互斥关系矩阵
- 基本用法
- 性能对比:读多写少场景
- [RWMutex 陷阱](#RWMutex 陷阱)
- [👥 sync.WaitGroup:等待组](#👥 sync.WaitGroup:等待组)
- [1️⃣ sync.Once:只执行一次](#1️⃣ sync.Once:只执行一次)
-
- 单例模式的标准实现
- [Once 的执行语义](#Once 的执行语义)
- [📢 sync.Cond:条件变量](#📢 sync.Cond:条件变量)
-
- 基本模式
- [Signal vs Broadcast](#Signal vs Broadcast)
- [⚠️ Cond 使用注意事项](#⚠️ Cond 使用注意事项)
- [⚛️ atomic:原子操作](#⚛️ atomic:原子操作)
-
- 常用原子操作
- [性能对比:atomic vs Mutex](#性能对比:atomic vs Mutex)
- [CAS 实现无锁栈](#CAS 实现无锁栈)
- [atomic vs Mutex 选型指南](#atomic vs Mutex 选型指南)
- [♻️ sync.Pool:临时对象池](#♻️ sync.Pool:临时对象池)
-
- 基本用法
- [实战:JSON 编码优化](#实战:JSON 编码优化)
- [⚠️ sync.Pool 注意事项](#⚠️ sync.Pool 注意事项)
- [🔗 errgroup:并发错误处理](#🔗 errgroup:并发错误处理)
-
- 基础用法
- WithContext:级联取消
- 限制并发数
- [errgroup 错误处理模式](#errgroup 错误处理模式)
- [📊 选型指南与最佳实践](#📊 选型指南与最佳实践)
- [💬 小结与互动](#💬 小结与互动)
-
- [🎯 速查表](#🎯 速查表)
- [💬 思考题](#💬 思考题)
🟡 Go入门到精通 | 同步原语:Go并发的工具箱
📅 更新于 2026年7月 | ✍️ 原创文章,转载请注明出处
🔒 为什么需要同步原语
💡 Channel 不是银弹。
虽然 Go 社区推崇 "不要通过共享内存来通信,而要通过通信来共享内存",但现实编程中,许多场景天然适合用锁和同步原语------比如保护一个被多个 Goroutine 读写的共享数据结构。
本文系统梳理 Go 1.26 中所有核心同步原语,帮你选对工具,少写 Bug。
🔐 sync.Mutex:互斥锁
基本用法
go
var mu sync.Mutex
var counter int
func increment() {
mu.Lock()
counter++
mu.Unlock()
}
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("最终计数:", counter) // 1000
}
⚡ defer 解锁防止死锁
go
// ✅ 推荐:defer 保证即使 panic 也能解锁
func safeIncrement() {
mu.Lock()
defer mu.Unlock() // 离开函数前自动解锁
counter++
// ... 复杂逻辑,可能提前 return 或 panic
}
// ❌ 危险:忘记解锁导致死锁
func dangerousIncrement() {
mu.Lock()
if someCondition {
return // 忘记 Unlock!死锁!
}
counter++
mu.Unlock()
}
Mutex 使用准则表
| 规则 | 说明 | 示例 |
|---|---|---|
| 🔒 Lock/Unlock 成对 | 一次 Lock 必须对应一次 Unlock | defer mu.Unlock() |
| 🚫 不可复制 | Mutex 复制后状态分离,锁失效 | 用指针传递 *sync.Mutex |
| 📦 嵌入结构体 | 非导出字段避免外部误操作 | type SafeMap struct { mu sync.Mutex; data map[string]int } |
| ⏱️ 锁粒度 | 锁住最小必要范围 | 锁外处理耗时 IO |
不可复制(Go vet 检查)
go
type Counter struct {
mu sync.Mutex
value int
}
// ❌ 值传递会复制 Mutex
func (c Counter) Bad() { // 值接收者!
c.mu.Lock()
c.value++
c.mu.Unlock()
} // 实际上操作的是一份副本,原对象根本没锁住
// ✅ 必须用指针接收者
func (c *Counter) Good() {
c.mu.Lock()
defer c.mu.Unlock()
c.value++
}
锁粒度优化示例
go
// ❌ 锁粒度过大:IO 操作在锁内
func (c *Counter) BadIncrement() {
c.mu.Lock()
defer c.mu.Unlock()
result := expensiveAPICall() // 耗时操作在锁内!
c.value += result
}
// ✅ 细粒度锁:只在必要时加锁
func (c *Counter) GoodIncrement() {
result := expensiveAPICall() // 锁外完成
c.mu.Lock()
c.value += result
c.mu.Unlock()
}
📖 sync.RWMutex:读写锁
读写锁是互斥锁的升级版:读操作不互斥,写操作互斥一切。
锁的互斥关系矩阵
| 🔓 无锁 | 📖 读锁 | ✍️ 写锁 | |
|---|---|---|---|
| 📖 加读锁 | ✅ 成功 | ✅ 成功 | ❌ 阻塞 |
| ✍️ 加写锁 | ✅ 成功 | ❌ 阻塞 | ❌ 阻塞 |
基本用法
go
type Cache struct {
mu sync.RWMutex
data map[string]string
}
func (c *Cache) Get(key string) (string, bool) {
c.mu.RLock() // 读锁------多个 goroutine 可同时持有
defer c.mu.RUnlock()
v, ok := c.data[key]
return v, ok
}
func (c *Cache) Set(key, value string) {
c.mu.Lock() // 写锁------排他
defer c.mu.Unlock()
c.data[key] = value
}
性能对比:读多写少场景
go
func benchmarkReadHeavy() {
cache := &Cache{data: make(map[string]string)}
cache.Set("key", "value")
var wg sync.WaitGroup
// 100 个并发读
for i := 0; i < 100; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for j := 0; j < 10000; j++ {
cache.Get("key")
}
}()
}
// 1 个写
wg.Add(1)
go func() {
defer wg.Done()
for j := 0; j < 100; j++ {
cache.Set("key", "new-value")
}
}()
wg.Wait()
}
🚀 结论 :在读写比 1000:1 的场景下,
RWMutex比Mutex性能高 10~50 倍。
RWMutex 陷阱
go
// ⚠️ 锁升级:持有读锁时不能加写锁(死锁!)
func (c *Cache) promote(key string) {
c.mu.RLock()
if c.data[key] == "old" {
c.mu.Lock() // ❌ 死锁!已持有读锁,无法升级为写锁
c.data[key] = "new"
c.mu.Unlock()
}
c.mu.RUnlock()
}
// ✅ 正确做法:先释放读锁,再加写锁
func (c *Cache) promoteCorrect(key string) {
c.mu.RLock()
needUpdate := c.data[key] == "old"
c.mu.RUnlock()
if needUpdate {
c.mu.Lock()
c.data[key] = "new"
c.mu.Unlock()
}
}
👥 sync.WaitGroup:等待组
协调一组 Goroutine 完成:Add 计数器 +1,Done 计数器 -1,Wait 阻塞直到计数器归零。
基础模式
go
func main() {
var wg sync.WaitGroup
urls := []string{"url1", "url2", "url3", "url4", "url5"}
for _, url := range urls {
wg.Add(1) // 计数器 +1
go func(u string) {
defer wg.Done() // 计数器 -1
fetch(u)
}(url) // ⚠️ 注意:必须传参或使用局部变量
}
wg.Wait() // 阻塞直到计数器归零
fmt.Println("所有任务完成")
}
⚠️ 经典陷阱:闭包变量捕获
go
// ❌ 错误:所有 goroutine 都用同一个 url 变量
for _, url := range urls {
wg.Add(1)
go func() {
defer wg.Done()
fetch(url) // 所有 goroutine 看到的 url 是最后一次的值!
}()
}
// ✅ 方案 1:传参
go func(u string) {
defer wg.Done()
fetch(u)
}(url)
// ✅ 方案 2:局部变量遮蔽
url := url // 在 goroutine 启动前创建副本
go func() {
defer wg.Done()
fetch(url)
}()
⚠️ Add 位置错误
go
// ❌ Add 在 goroutine 内部------可能 Wait 先于 Add 执行
go func() {
wg.Add(1) // 太晚了!
defer wg.Done()
doWork()
}()
// ✅ Add 在 goroutine 启动前
wg.Add(1)
go func() {
defer wg.Done()
doWork()
}()
错误收集模式
go
func parallelFetch(urls []string) []error {
var wg sync.WaitGroup
errCh := make(chan error, len(urls))
for _, url := range urls {
wg.Add(1)
go func(u string) {
defer wg.Done()
if err := fetch(u); err != nil {
errCh <- err
}
}(url)
}
go func() {
wg.Wait()
close(errCh)
}()
var errs []error
for err := range errCh {
errs = append(errs, err)
}
return errs
}
1️⃣ sync.Once:只执行一次
单例模式的标准实现
go
type Config struct {
DBHost string
DBPort int
}
var (
instance *Config
once sync.Once
)
func GetConfig() *Config {
once.Do(func() {
// 这段代码只会执行一次,即使多个 goroutine 同时调用
fmt.Println("初始化配置...")
instance = &Config{
DBHost: "localhost",
DBPort: 5432,
}
})
return instance
}
func main() {
// 100 个 goroutine 同时获取配置
for i := 0; i < 100; i++ {
go func() {
cfg := GetConfig()
fmt.Println(cfg.DBHost)
}()
}
}
Once 的执行语义
| 特性 | 说明 |
|---|---|
| 🔒 线程安全 | 多个 goroutine 同时调用 Do(),只有一个执行 |
| 🚫 不可重复 | Do() 中的函数只执行一次,后续调用直接返回 |
| ⚠️ panic 不影响 | 函数 panic 后 Once 仍认为已执行,不会重试 |
| 📦 不可复制 | 同 Mutex,必须用指针 |
💡 使用场景:初始化数据库连接池、加载配置文件、注册全局组件。
📢 sync.Cond:条件变量
sync.Cond 是 Go 中最少被提及但非常强大的同步原语,用于等待某个条件满足后唤醒等待的 goroutine。
基本模式
go
type Queue struct {
mu sync.Mutex
cond *sync.Cond
items []int
cap int
}
func NewQueue(cap int) *Queue {
q := &Queue{cap: cap}
q.cond = sync.NewCond(&q.mu)
return q
}
// 生产者:队列满时等待
func (q *Queue) Produce(item int) {
q.mu.Lock()
for len(q.items) == q.cap { // ⚠️ 必须用 for 而非 if
q.cond.Wait() // 等待消费者唤醒
}
q.items = append(q.items, item)
fmt.Println("生产:", item)
q.cond.Signal() // 唤醒一个等待的消费者
q.mu.Unlock()
}
// 消费者:队列空时等待
func (q *Queue) Consume() int {
q.mu.Lock()
for len(q.items) == 0 {
q.cond.Wait() // 等待生产者唤醒
}
item := q.items[0]
q.items = q.items[1:]
fmt.Println("消费:", item)
q.cond.Signal()
q.mu.Unlock()
return item
}
Signal vs Broadcast
| 方法 | 效果 | 适用场景 |
|---|---|---|
🔔 Signal() |
唤醒一个等待的 goroutine | 每次只需一个 goroutine 处理 |
📢 Broadcast() |
唤醒所有等待的 goroutine | 全局状态变更,所有等待者都应重新检查 |
go
// Broadcast 示例:配置热更新
type HotConfig struct {
mu sync.Mutex
cond *sync.Cond
data map[string]string
}
func (hc *HotConfig) Reload(newData map[string]string) {
hc.mu.Lock()
hc.data = newData
hc.mu.Unlock()
hc.cond.Broadcast() // 通知所有等待者配置已更新
}
func (hc *HotConfig) WaitForUpdate(oldVersion int) map[string]string {
hc.mu.Lock()
hc.cond.Wait() // 等待配置更新
data := hc.data
hc.mu.Unlock()
return data
}
⚠️ Cond 使用注意事项
- 必须用 for 而非 if:Wait() 可能被虚假唤醒(spurious wakeup)
- Wait() 前必须持有锁:Wait() 内部会释放锁然后阻塞,被唤醒后重新获取锁
- Signal 不保证顺序:FIFO 不保证,只保证唤醒一个
⚛️ atomic:原子操作
对于简单的计数器、标志位,sync/atomic 比 Mutex 轻量得多。
常用原子操作
go
import "sync/atomic"
// 原子加法
var counter int64
atomic.AddInt64(&counter, 1) // counter += 1
atomic.AddInt64(&counter, -1) // counter -= 1
// 原子读取 & 存储
var flag int32
atomic.StoreInt32(&flag, 1) // flag = 1
val := atomic.LoadInt32(&flag) // 读取 flag
// CAS (Compare And Swap):无锁编程的核心
// 如果 addr 的值等于 old,则设为 new,返回是否成功
var state int64
swapped := atomic.CompareAndSwapInt64(&state, 0, 1)
// 原子值:存储任意类型
var config atomic.Value
config.Store(&Config{DBHost: "localhost"})
cfg := config.Load().(*Config)
性能对比:atomic vs Mutex
go
// atomic 实现计数器
func atomicCounter() {
var counter int64
var wg sync.WaitGroup
for i := 0; i < 10000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
atomic.AddInt64(&counter, 1)
}()
}
wg.Wait()
}
// Mutex 实现计数器
func mutexCounter() {
var mu sync.Mutex
var counter int64
var wg sync.WaitGroup
for i := 0; i < 10000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
mu.Lock()
counter++
mu.Unlock()
}()
}
wg.Wait()
}
// atomic 版本快 2~5 倍(竞争越激烈差距越大)
CAS 实现无锁栈
go
type LockFreeStack struct {
top unsafe.Pointer // *node
}
type node struct {
value int
next unsafe.Pointer // *node
}
func (s *LockFreeStack) Push(v int) {
n := &node{value: v}
for {
oldTop := atomic.LoadPointer(&s.top)
n.next = oldTop
if atomic.CompareAndSwapPointer(&s.top, oldTop, unsafe.Pointer(n)) {
return
}
// CAS 失败,说明被其他 goroutine 抢先了,重试
}
}
func (s *LockFreeStack) Pop() (int, bool) {
for {
oldTop := atomic.LoadPointer(&s.top)
if oldTop == nil {
return 0, false
}
next := (*node)(oldTop).next
if atomic.CompareAndSwapPointer(&s.top, oldTop, next) {
return (*node)(oldTop).value, true
}
}
}
atomic vs Mutex 选型指南
| 场景 | 推荐 | 原因 |
|---|---|---|
| 🔢 简单计数器 | atomic |
极快,无锁 |
| 🚩 布尔标志位 | atomic |
一条指令搞定 |
| 🔄 复杂数据结构 | Mutex |
多个字段需要一致性 |
| 📊 高频读写 | atomic 或 RWMutex |
看读写比例 |
♻️ sync.Pool:临时对象池
复用高频创建销毁的对象,减少 GC 压力。
基本用法
go
var bufferPool = sync.Pool{
New: func() interface{} {
return make([]byte, 4096) // 池子空时创建新的
},
}
func process(data []byte) {
buf := bufferPool.Get().([]byte)
defer bufferPool.Put(buf) // 用完归还
// 使用 buf 处理...
copy(buf, data)
doSomething(buf)
}
实战:JSON 编码优化
go
var encoderPool = sync.Pool{
New: func() interface{} {
return json.NewEncoder(nil)
},
}
func encode(w io.Writer, v interface{}) error {
enc := encoderPool.Get().(*json.Encoder)
defer encoderPool.Put(enc)
// 重置 encoder(Go 1.26 无 Reset 方法时需重新创建 writer)
// 可以在自定义 wrapper 中实现
return enc.Encode(v)
}
⚠️ sync.Pool 注意事项
| 注意点 | 说明 |
|---|---|
| 🗑️ GC 会清空池 | Pool 中的对象会在 GC 时被回收,不能依赖池中永远有对象 |
| 📏 对象大小 | 适合临时对象,不适合持久化数据 |
| ⏳ 生命周期 | 每次 Get 后必须 Put,不然 Pool 失去意义 |
🔗 errgroup:并发错误处理
golang.org/x/sync/errgroup 提供了WaitGroup + 错误传播的能力。
基础用法
go
import "golang.org/x/sync/errgroup"
func fetchAll(urls []string) error {
g := new(errgroup.Group)
for _, url := range urls {
url := url // 闭包变量捕获
g.Go(func() error {
resp, err := http.Get(url)
if err != nil {
return fmt.Errorf("请求 %s 失败: %w", url, err)
}
defer resp.Body.Close()
// 处理响应...
return nil
})
}
// Wait 返回第一个错误,同时取消其他 goroutine
if err := g.Wait(); err != nil {
return fmt.Errorf("批量请求失败: %w", err)
}
return nil
}
WithContext:级联取消
go
func fetchAllWithCancel(ctx context.Context, urls []string) error {
g, ctx := errgroup.WithContext(ctx)
for _, url := range urls {
url := url
g.Go(func() error {
req, _ := http.NewRequestWithContext(ctx, "GET", url, nil)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
return nil
})
}
return g.Wait()
// 任何一个 goroutine 返回错误,ctx 被取消,
// 其他 goroutine 的 HTTP 请求也会终止
}
限制并发数
go
func fetchAllLimited(urls []string, limit int) error {
g := new(errgroup.Group)
g.SetLimit(limit) // Go 1.20+ 支持,限制同时运行的 goroutine 数
for _, url := range urls {
url := url
g.Go(func() error {
return fetch(url)
})
}
return g.Wait()
}
errgroup 错误处理模式
go
// 收集所有错误而非只取第一个
func fetchAllCollectErrors(urls []string) []error {
g := new(errgroup.Group)
errMu := sync.Mutex{}
var errs []error
for _, url := range urls {
url := url
g.Go(func() error {
if err := fetch(url); err != nil {
errMu.Lock()
errs = append(errs, err)
errMu.Unlock()
}
return nil // 不返回错误,继续执行其他任务
})
}
g.Wait()
return errs
}
📊 选型指南与最佳实践
同步原语选型决策树
需要协调多个 Goroutine?
├── 等待所有 Goroutine 完成?
│ └── ✅ sync.WaitGroup
├── 保护共享数据?
│ ├── 简单计数器/标志位?
│ │ └── ✅ atomic
│ ├── 读多写少?
│ │ └── ✅ sync.RWMutex
│ └── 读写均衡 + 逻辑复杂?
│ └── ✅ sync.Mutex
├── 等待条件满足?
│ └── ✅ sync.Cond
├── 只初始化一次?
│ └── ✅ sync.Once
├── 并发任务 + 错误传播?
│ └── ✅ errgroup
└── 复用对象减少 GC?
└── ✅ sync.Pool
最佳实践清单
| ✅ 实践 | 说明 |
|---|---|
🔒 defer mu.Unlock() |
防止忘记解锁 |
| 📍 锁粒度最小化 | IO 操作不要放锁内 |
| 🚫 不可复制锁 | 用指针或嵌入结构体 |
| ⚠️ WaitGroup.Add 在 goroutine 外 | 避免竞态 |
| ♻️ Pool.Put 后不访问 | 对象随时可能被其他 goroutine 取走 |
| 🔄 RWMutex 不可升级 | 释放读锁后才能加写锁 |
💬 小结与互动
🎯 速查表
| 原语 | 核心功能 | 经典场景 |
|---|---|---|
sync.Mutex |
互斥锁 | 保护共享数据结构 |
sync.RWMutex |
读写锁 | 读多写少缓存 |
sync.WaitGroup |
等待组 | 并发任务协调 |
sync.Once |
一次执行 | 单例初始化 |
sync.Cond |
条件变量 | 生产者-消费者 |
atomic |
原子操作 | 计数器、标志位 |
sync.Pool |
对象池 | 高频临时对象复用 |
errgroup |
错误传播 | 批量并发任务 |
💬 思考题
- 以下代码有什么隐患?
go
var mu sync.Mutex
func update(m map[string]int) {
mu.Lock()
m["count"]++
mu.Unlock()
}
👆 点击查看答案 `update` 接受的是 map 的引用,没问题。但如果有其他地方直接访问这个 map 而没有先加锁,就会 data race。应该将所有对 map 的访问封装到带锁的方法中。
- 为什么 sync.Cond.Wait() 必须放在 for 循环里?
👆 点击查看答案 两个原因:(1) 虚假唤醒------操作系统可能错误唤醒等待的线程;(2) Broadcast 唤醒所有等待者,但可能只有一个能满足条件,其他需要继续等待。
📖 下一篇预告:Pipeline 管道模式、Generator 生成器、生产者-消费者...Go 并发模式(上)!
作者:布朗克168 | 系列:Go入门到精通 2026