Go 语言并发编程的 “工具箱”

Go 语言并发编程的 "工具箱"

sync 包是 Go 语言并发编程的 "工具箱",里面的每一个 API 都是为了解决特定的并发问题设计的。

基础协作:WaitGroup(等待组)

作用:等待一组 goroutine 全部执行完毕,是最常用的 "并发等待" 工具。

原理 :内部是一个计数器,Add(n) 增加计数,Done() 减少计数,Wait() 阻塞直到计数归零。

go 复制代码
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 必须等解锁后才能加锁。

go 复制代码
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()):写锁与读锁、写锁与写锁都互斥(写操作时,不能读也不能写)。
go 复制代码
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 一次。

go 复制代码
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):遍历所有键值对。
go 复制代码
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 函数创建新对象。

go 复制代码
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 一起用
go 复制代码
// 生产者 - 消费者模式
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 条件变量,协调等待 - 通知 复杂的生产者 - 消费者、需要广播通知的场景
选择原则
  1. 优先用简单的WaitGroup > Mutex > RWMutex > Map > Pool > Cond
  2. 读多写少 :优先考虑 RWMutex(锁)或 Map(并发 Map);
  3. 性能优化 :频繁创建大对象时用 Pool
  4. 复杂协调 :channel 解决不了的场景再用 Cond
相关推荐
用户8356290780512 小时前
Python 实现 PowerPoint 形状动画设置
后端·python
用户908324602733 小时前
Spring Boot 缓存架构:一行配置切换 Caffeine 与 Redis,透明支持多租户隔离
后端
tyung3 小时前
zhenyi-base 开源 | Go 高性能基础库:TCP 77万 QPS,无锁队列 16ns/op
后端·go
子兮曰3 小时前
Humanizer-zh 实战:把 AI 初稿改成“能发布”的技术文章
前端·javascript·后端
桦说编程3 小时前
你的函数什么颜色?—— 深入理解异步编程的本质问题(上)
后端·性能优化·编程语言
百度地图汽车版4 小时前
【AI地图 Tech说】第九期:让智能体拥有记忆——打造千人千面的小度想想
前端·后端
臣妾没空4 小时前
Elpis 全栈框架:从构建到发布的完整实践总结
前端·后端
H5开发新纪元4 小时前
Nginx 部署 Vue3 项目完整指南
前端·javascript·面试
喷火龙8号4 小时前
单 Token 认证方案的进阶优化:透明刷新机制
后端·架构