Go 并发编程

📅 2026/7/11 19:49:01
Go 并发编程
Go 并发编程从原理到实战本文基于 Go 并发核心知识体系整理面向已有编程基础、希望系统掌握 Go 并发的开发者。一、Go 并发模型的核心设计1.1 并发 vs 并行概念本质硬件要求并发 (Concurrency)多任务在同一时间段内交替执行单核即可并行 (Parallelism)多任务在同一时刻真正同时执行必须多核Go 的并发哲学基于CSPCommunicating Sequential Processes模型不要通过共享内存来通信而要通过通信来共享内存。传统多线程Java/C多个线程访问同一块内存 → 用锁保护 Go 方式每个 goroutine 独立工作 → 通过channel传递消息1.2 GMP 调度模型Go 的调度器使用 GMP 模型G Goroutine任务M Machine系统线程P Processor逻辑处理器默认 CPU 核心数Go 运行时负责把大量的 G 分配到少量的 M 上执行P 负责调度。这就是为什么 goroutine 能轻松创建上万个而不会压垮系统。对比项goroutine系统线程创建成本极低约 2KB 栈空间高约 1MB 栈空间调度方式Go 运行时自己调度GMP 模型操作系统内核调度切换成本低用户态切换高内核态切换数量上限可轻松创建数十万个几千个就吃力二、Goroutine轻量级并发单元2.1 启动与生命周期package main ​ import ( fmt sync time ) ​ func worker(id int, wg *sync.WaitGroup) { defer wg.Done() fmt.Printf(Worker %d starting , id) time.Sleep(time.Second) fmt.Printf(Worker %d done , id) } ​ func main() { var wg sync.WaitGroup ​ for i : 1; i 3; i { wg.Add(1) go worker(i, wg) } ​ wg.Wait() fmt.Println(All workers completed) }核心规则main函数本身是一个 goroutine主协程。主协程结束时所有子 goroutine 会被强制终止。2.2 WaitGroup 三要素wg.Add(n) // 计数器 n启动 n 个任务前调用 wg.Done() // 计数器 -1任务完成时调用推荐 defer wg.Wait() // 阻塞等待直到计数器归零2.3 闭包陷阱必知// ❌ 错误所有 goroutine 可能输出相同值 for i : 1; i 5; i { go func() { fmt.Println(i) // i 被复用可能全是 6 }() } ​ // ✅ 正确通过参数传递副本 for i : 1; i 5; i { go func(n int) { fmt.Println(n) // 每个 goroutine 有自己的 n }(i) }三、Channel通信而非共享3.1 无缓冲 vs 有缓冲ch1 : make(chan int) // 无缓冲容量 0同步通信 ch2 : make(chan int, 10) // 有缓冲容量 10异步通信特性无缓冲 Channel有缓冲 Channel发送条件必须有接收者就绪缓冲区未满即可接收条件必须有发送者就绪缓冲区非空即可本质同步——发送和接收配对异步——满/空时才阻塞用途同步协调、信号通知生产者-消费者、削峰填谷3.2 生产者-消费者模式package main ​ import fmt ​ func producer(ch chan- int, done chan- bool) { for i : 1; i 5; i { ch - i fmt.Printf(Produced: %d , i) } close(ch) done - true } ​ func consumer(ch -chan int) { for val : range ch { // range 自动检测 close取完退出 fmt.Printf(Consumed: %d , val) } } ​ func main() { ch : make(chan int, 2) // 缓冲 2 个 done : make(chan bool) ​ go producer(ch, done) go consumer(ch) ​ -done }Channel 方向约束编译期安全检查chan- T只发送send-only-chan T只接收receive-onlychan T双向3.3 关闭 Channel 的规范// ✅ 只有发送方关闭接收方检测 close(ch) ​ // 接收检测方式 1ok 模式 val, ok : -ch if !ok { // channel 已关闭且无数据 } ​ // 接收检测方式 2range 模式推荐 for val : range ch { // 自动在 close 且取完后退出 }⚠️ 危险操作向已关闭的 channel 发送 →panic重复关闭 channel →panic未关闭的 channel 用range→ 死锁四、Select多路复用4.1 基本语法select { case v1 : -ch1: // ch1 有数据时执行 case v2 : -ch2: // ch2 有数据时执行 case ch3 - 42: // 可以往 ch3 发送 default: // 所有 case 都不满足时执行非阻塞 }核心规则阻塞直到某个 case 可执行多个 case 同时满足 →随机选择一个无default且全部阻塞 →select阻塞空select{}→ 永久阻塞死锁4.2 超时控制select { case result : -callAPI(): fmt.Println(成功:, result) case -time.After(2 * time.Second): fmt.Println(请求超时) }4.3 非阻塞操作select { case v : -ch: fmt.Println(received:, v) default: // 没有数据也不阻塞立即执行 fmt.Println(no data available) }五、Context跨 goroutine 的取消与传值5.1 为什么需要 Context假设这样一个场景你启动了一个 goroutine 去做下载任务下载过程中用户点了取消。怎么通知这个 goroutine 停下来之前的方式用 channel 传递信号stop : make(chan struct{}) ​ go func() { for { select { case -stop: return // 收到停止信号 default: // 继续下载 } } }() ​ close(stop) // 通知停止但存在以下问题多个 goroutine 嵌套时信号传递很麻烦无法设置超时时间无法传递请求级别的数据如 trace ID、用户 IDContext 就是 Go 官方给的解决方案专门用于取消信号传递协程树传播超时控制传递请求级数据5.2 Context 接口与创建type Context interface { Deadline() (deadline time.Time, ok bool) Done() -chan struct{} // 取消时关闭通过 -ctx.Done() 感知 Err() error // 取消原因Canceled / DeadlineExceeded Value(key any) any // 获取键值对 }// 根 Context永不取消 ctx : context.Background() ctx : context.TODO() // 不确定时占位 ​ // 派生 Context ctx, cancel : context.WithCancel(parent) // 手动取消 ctx, cancel : context.WithTimeout(parent, d) // 超时自动取消 ctx, cancel : context.WithDeadline(parent, t) // 截止时间点 ctx context.WithValue(parent, key, val) // 传值不可变5.3 WithCancel主动取消func main() { ctx, cancel : context.WithCancel(context.Background()) ​ go func() { for { select { case -ctx.Done(): // 监听到取消信号 fmt.Println(子 goroutine收到取消信号退出) return default: fmt.Println(子 goroutine工作中...) time.Sleep(500 * time.Millisecond) } } }() ​ time.Sleep(2 * time.Second) fmt.Println(main我要取消了) cancel() // 发送取消信号 ​ time.Sleep(500 * time.Millisecond) fmt.Println(main退出) }执行流程context.WithCancel(parent)返回一个ctx和cancel函数子 goroutine 通过-ctx.Done()监听取消信号main 调用cancel()时ctx.Done()返回的 channel 被关闭所有监听这个 ctx 的 goroutine 都会收到取消通知context.WithCancel创建的是一个派生 Context它继承父 Context 的所有属性。父 Context 取消时所有子 Context 也会一起取消。5.4 WithTimeout超时取消func main() { // 1 秒后自动取消 ctx, cancel : context.WithTimeout(context.Background(), 1*time.Second) defer cancel() // 养成好习惯用完释放资源 ​ go func() { for { select { case -ctx.Done(): fmt.Println(子 goroutine超时了退出) return default: fmt.Println(子 goroutine工作中...) time.Sleep(200 * time.Millisecond) } } }() ​ time.Sleep(2 * time.Second) fmt.Println(main退出) }WithTimeout等价于WithDeadline指定一个未来时间点。WithTimeout(ctx, 1*time.Second)WithDeadline(ctx, time.Now().Add(1*time.Second))5.5 WithDeadline指定截止时间func main() { deadline : time.Now().Add(2 * time.Second) ctx, cancel : context.WithDeadline(context.Background(), deadline) defer cancel() ​ // 工作... for { select { case -ctx.Done(): fmt.Println(到截止时间了退出) return default: fmt.Println(还在工作...) time.Sleep(500 * time.Millisecond) } } }5.6 WithValue传递请求级数据func main() { ctx : context.Background() // 链式添加值 ctx context.WithValue(ctx, userID, 12345) ctx context.WithValue(ctx, traceID, abc-xyz) ​ handleRequest(ctx) } ​ func handleRequest(ctx context.Context) { userID : ctx.Value(userID) traceID : ctx.Value(traceID) fmt.Printf(处理请求用户 %v追踪ID %v , userID, traceID) }重要注意WithValue的 key 推荐使用自定义类型避免字符串冲突type contextKey string ​ const ( KeyUserID contextKey userID KeyTraceID contextKey traceID ) ​ ctx context.WithValue(ctx, KeyUserID, 12345)Context 中的值应该是请求级别的、非关键的数据如 trace ID、用户身份不要用 Context 传递函数参数5.7 Context 传递规则context.Background() └── WithCancel → ctx1, cancel1 ├── WithTimeout → ctx2, cancel21秒后自动取消 │ └── ctx2 取消时ctx1 不受影响 └── WithValue → ctx3 └── ctx3 中可以读取 ctx1 的值但不能读取 ctx2 的值没有继承关系核心规则链式传递只向下层传播不向上层传播。父 Context 取消 → 所有派生的子 Context 都取消子 Context 取消 → 不影响父 ContextWithValue只在链上向后传递5.8 综合示例goroutine 树的层级取消package main ​ import ( context fmt time ) ​ func worker(ctx context.Context, name string) { for { select { case -ctx.Done(): fmt.Printf(%s被取消了原因%v , name, ctx.Err()) return default: fmt.Printf(%s工作中... , name) time.Sleep(500 * time.Millisecond) } } } ​ func main() { // 根 Context ctxRoot : context.Background() ​ // 第一层可取消 ctxLevel1, cancel1 : context.WithCancel(ctxRoot) ​ // 第二层3 秒超时 ctxLevel2, cancel2 : context.WithTimeout(ctxLevel1, 3*time.Second) defer cancel2() ​ // 启动 3 个 worker go worker(ctxLevel1, worker-Alevel1) go worker(ctxLevel2, worker-Blevel23秒超时) go worker(ctxLevel2, worker-Clevel23秒超时) ​ // 5 秒后取消 level1 time.Sleep(5 * time.Second) fmt.Println( main 取消 level1 ) cancel1() ​ time.Sleep(1 * time.Second) fmt.Println(main 退出) }运行流程分析时间事件0s启动 worker-A监听 level1、worker-B/C监听 level23slevel2 超时 → worker-B/C 退出ctx.Err() context deadline exceeded3~5sworker-A 仍在工作5scancel1()→ worker-A 退出ctx.Err() context canceled六、并发安全锁与原子操作6.1 竞态条件Race Condition多个 goroutine同时读写同一个变量没有同步保护结果取决于执行顺序——这叫竞态条件。看一个经典的例子var counter 0 ​ func main() { for i : 0; i 1000; i { go func() { counter // 多个 goroutine 同时操作 }() } time.Sleep(time.Second) fmt.Println(counter) // 期望 1000实际 }运行多次每次结果不一样可能是 962、988、1001...为什么counter不是安全的counter看起来是一行代码但在 CPU 层面是三步counter 实际是 ① LOAD → 从内存读取 counter 的值 ② ADD → 加 1 ③ STORE → 把新值写回内存两个 goroutine 同时执行时可能这样goroutine ALOAD(0) → ADD → 正要 STORE(1) ← CPU 切换到 goroutine B → goroutine BLOAD(0) → ADD → STORE(1) ← counter 1 ← CPU 切换回 goroutine A → goroutine ASTORE(1) ← 又写了一次 1覆盖了 B 的结果加了两次结果只加了 1。这叫丢失更新。用go run -race检测竞争go run -race main.go输出会告诉你哪两个 goroutine 在争抢哪个变量 WARNING: DATA RACE Read at 0x... by goroutine X main.main.func1() ​ Previous write at 0x... by goroutine Y main.main.func1() 开发阶段一定要加-race跑一遍。6.2 sync.Mutex 互斥锁sync.Mutex像厕所门锁——一个人进去锁上门其他人排队等着。var ( counter int mu sync.Mutex // 声明一个互斥锁 ) ​ func main() { for i : 0; i 1000; i { go func() { mu.Lock() // ① 上锁如果锁已被别人持有就阻塞等 counter // ② 安全操作共享变量 mu.Unlock() // ③ 解锁排队的人可以进来了 }() } time.Sleep(time.Second) fmt.Println(counter) // 稳定输出 1000 }基本原则mu.Lock() ... 操作共享变量 ... mu.Unlock()三句话记牢锁要成对Lock 和 Unlock 必须配对忘记 Unlock 会导致死锁范围要小只在操作共享变量的地方加锁不要锁整个函数defer 保底函数内有多个 return 时用 defer 确保解锁// ✅ 推荐用 defer 确保解锁 func safeAdd() { mu.Lock() defer mu.Unlock() counter // 可能还有别的操作... } ​ // ❌ 不推荐手动解锁容易忘 func badAdd() { mu.Lock() counter mu.Unlock() // 如果中间有 panic 或提前 return就死锁了 }典型错误死锁mu.Lock() mu.Lock() // 同一协程重复锁同一个 Mutex → 死锁Go 的sync.Mutex是不可重入的——同一个 goroutine 不能对自己已持有的 Mutex 再次 Lock会永远等自己解锁死锁。6.3 sync.RWMutex 读写锁sync.Mutex是互斥锁——不管是读还是写一次只能一个人用。但实际场景往往是读多写少。比如一个配置中心1000 个 goroutine 读配置1 个 goroutine 更新配置如果用sync.Mutex1000 个读的人也要互相排队明明读操作不会破坏数据白白浪费性能。sync.RWMutex就是优化这个的多个可以同时读但写的时候谁都别来。var ( config map[string]string rwmu sync.RWMutex ) ​ // 读操作用 RLock/RUnlock func getConfig(key string) string { rwmu.RLock() // 读锁——其他读者可以同时进来 defer rwmu.RUnlock() return config[key] } ​ // 写操作用 Lock/Unlock func setConfig(key, value string) { rwmu.Lock() // 写锁——所有人都不许进来 defer rwmu.Unlock() config[key] value }RWMutex 规则操作别人能读吗别人能写吗读锁 (RLock)被持有✅ 随便读❌ 不能写写锁 (Lock)被持有❌ 不能读❌ 不能写读锁不互斥写锁全互斥。rwmu.RLock() // A 拿读锁 rwmu.RLock() // B 也能拿读锁 ✅ 同时读 rwmu.Lock() // C 想拿写锁 ❌ 等 A 和 B 都释放读锁后才能拿 ​ rwmu.Lock() // A 拿写锁 rwmu.RLock() // B 想拿读锁 ❌ 等 A 释放写锁 rwmu.Lock() // C 想拿写锁 ❌ 等 A 释放写锁6.4 sync/atomic 原子操作为什么有了锁还要原子操作atomic比 Mutex更轻量——它直接利用 CPU 指令级别的原子性不需要操作系统线程调度。Mutexatomic机制操作系统调度CPU 指令保证性能慢上下文切换快一条指令适用复杂操作多个变量、if-check简单计数器、标记位常用函数var counter int64 ​ // 原子加 atomic.AddInt64(counter, 1) // counter 1 atomic.AddInt64(counter, -1) // counter - 1 ​ // 原子读写 atomic.StoreInt64(counter, 100) // counter 100 val : atomic.LoadInt64(counter) // val counter ​ // 原子交换 old : atomic.SwapInt64(counter, 200) // 旧值返回新值设 200 ​ // 比较并交换CAS // 如果 counter 100就设为 200返回 true否则返回 false swapped : atomic.CompareAndSwapInt64(counter, 100, 200)用 atomic 修复计数器的竞争var counter int64 ​ func main() { for i : 0; i 1000; i { go func() { atomic.AddInt64(counter, 1) // CPU 级别原子加 }() } time.Sleep(time.Second) fmt.Println(atomic.LoadInt64(counter)) // 稳定 1000 }不需要加锁一行atomic.AddInt64解决。CAS 的典型用途自旋锁 / 只执行一次var initialized int32 ​ func initConfig() { // 只有第一次调用会进入 if atomic.CompareAndSwapInt32(initialized, 0, 1) { fmt.Println(初始化配置...) // 做初始化... } }6.5 sync.Once一次性初始化sync.Once确保某段代码在整个程序生命周期中只执行一次无论被调用多少次。var once sync.Once ​ func initConfig() { once.Do(func() { fmt.Println(初始化配置...) // 只执行一次 }) } ​ func main() { go initConfig() // 第 1 次执行 go initConfig() // 第 2 次跳过 go initConfig() // 第 3 次跳过 time.Sleep(time.Second) }输出只有一行初始化配置...sync.Once vs atomic 做一次性初始化// 用 Once推荐 var once sync.Once once.Do(func() { ... }) ​ // 用 atomic CAS手动实现 var done int32 if atomic.CompareAndSwapInt32(done, 0, 1) { ... }sync.Once内部就是用了 atomic但封装得更安全——如果你传的函数里 panic 了sync.Once会计数为已完成不会重复执行。典型场景单例模式var ( instance *Database once sync.Once ) ​ func GetDatabase() *Database { once.Do(func() { instance Database{/* 初始化 */} }) return instance }6.6 工具选择速查工具使用场景一句话Mutex对共享变量的复杂操作厕所门锁一次进一个RWMutex读多写少场景多人可同时读写时全不让atomic简单的计数器、标记位CPU 级原子操作比锁快Once只初始化一次一辈子只干一次的活go run -race检测竞态条件开发必加选择原则需要保护共享变量 ├── 只是简单的 int64 计数 → atomic ├── 读多写少 → RWMutex ├── 只需初始化一次 → Once └── 其他复杂操作 → Mutex七、Worker Pool 实战控制并发数量避免资源耗尽package main ​ import ( fmt sync time ) ​ func worker(id int, jobs -chan int, results chan- int) { for job : range jobs { fmt.Printf(工人 %d开始任务 %d , id, job) time.Sleep(time.Second) // 模拟工作 results - job * 2 // 返回结果 fmt.Printf(工人 %d完成任务 %d , id, job) } } ​ func main() { const numJobs 5 jobs : make(chan int, numJobs) results : make(chan int, numJobs) ​ // 启动 3 个工人goroutine for i : 1; i 3; i { go worker(i, jobs, results) } ​ // 派发 5 个任务 for i : 1; i numJobs; i { jobs - i } close(jobs) // 通知工人没有新任务了 ​ // 收集 5 个结果 for i : 1; i numJobs; i { -results } }关键理解3 个工人并发处理 5 个任务谁空闲谁领任务。八、常见陷阱与最佳实践8.1 Goroutine 泄漏// ❌ 错误如果 ctx 超时这个 goroutine 永远阻塞在 ch 上 func bad(ctx context.Context, ch chan int) { go func() { val : -ch // 泄漏 fmt.Println(val) }() } ​ // ✅ 正确配合 select 监听取消 func good(ctx context.Context, ch chan int) { go func() { select { case val : -ch: fmt.Println(val) case -ctx.Done(): return // 及时退出 } }() }8.2 Channel 使用规范操作规则关闭只有发送方应该关闭 channel发送向已关闭的 channel 发送 →panic接收已关闭且无数据 → 返回零值用ok或range检测nil channel发送/接收都会永久阻塞可用于 select 禁用某个 case8.3 性能优化 checklist需要保护共享变量 ├── 只是简单的 int64 计数/标记 → atomic ├── 读多写少 → RWMutex ├── 只需初始化一次 → sync.Once ├── 并发安全 map只写一次读多次 → sync.Map └── 其他复杂操作 → Mutex 需要控制并发数量 → channel 作为信号量 需要等待一组任务 → sync.WaitGroup 需要超时/取消 → context.Context九、总结Go 的并发设计以简洁和高效著称核心工具链如下工具用途记忆口诀goroutine轻量级并发单元go一按就起飞channelgoroutine 间通信不要共享内存要通信select多路复用谁先来就处理谁context取消/超时/传值cancel 一按全停掉Mutex互斥保护厕所门锁一次一个RWMutex读多写少多人可读一写全让atomic轻量计数CPU 级原子比锁快sync.Once一次性初始化一辈子只干一次