开篇
Don’t communicate by sharing memory; share memory by communicating.
— Effective Go
Go 语言的并发哲学围绕一句名言展开:不要通过共享内存来通信,而要通过通信来共享内存。这篇文章将通过三个递进的实战示例,展示如何将 goroutine、channel 和 context.Context 组合成健壮的并发程序。
基础组件速览
Goroutine
Goroutine 是 Go 的轻量级用户态线程,由 Go 运行时(GMP 模型)调度。启动一个只需要 go 关键字:
1package main
2
3import (
4 "fmt"
5 "time"
6)
7
8func main() {
9 go func() {
10 fmt.Println("Hello from goroutine!")
11 }()
12
13 time.Sleep(10 * time.Millisecond) // 等待 goroutine 执行完毕
14}
GMP 模型中,每个 P(Processor)绑定一个 M(Machine/OS thread),调度本地运行队列中的 G(Goroutine)。核心数据结构大致如下:
1// runtime/runtime2.go (简化示意)
2type g struct {
3 stack stack // goroutine 栈
4 sched gobuf // 调度上下文 (sp, pc, bp...)
5 atomicstatus uint32 // _Gidle / _Grunning / _Gwaiting...
6}
7
8type p struct {
9 runq [256]guintptr // 本地运行队列
10 runnext guintptr // 下一个优先运行的 G
11 m muintptr // 当前绑定的 M
12}
Channel
Channel 是 goroutine 之间传递数据的类型安全管道。make(chan T, n) 创建缓冲区容量为 n 的 Channel。
- 无缓冲 channel:
ch := make(chan int)— 发送方阻塞直到接收方就绪(同步) - 有缓冲 channel:
ch := make(chan int, 10)— 缓冲区满之前不阻塞(异步)
1// Fan-in pattern: 多路输入合并为单路输出
2func fanIn(ch1, ch2 <-chan string) <-chan string {
3 out := make(chan string)
4 go func() {
5 for {
6 select {
7 case s := <-ch1:
8 out <- s
9 case s := <-ch2:
10 out <- s
11 }
12 }
13 }()
14 return out
15}
实战一:带超时控制的并发爬虫
结合 context.WithTimeout 和 sync.WaitGroup 实现可控的并发网络请求:
1package crawler
2
3import (
4 "context"
5 "fmt"
6 "net/http"
7 "sync"
8 "time"
9)
10
11type Result struct {
12 URL string
13 StatusCode int
14 Duration time.Duration
15 Err error
16}
17
18func Crawl(ctx context.Context, urls []string, concurrency int) []Result {
19 limiter := make(chan struct{}, concurrency) // 并发度控制
20 var wg sync.WaitGroup
21 results := make([]Result, 0, len(urls))
22 var mu sync.Mutex
23
24 for _, u := range urls {
25 select {
26 case <-ctx.Done():
27 break // 全局超时,不再启动新请求
28 default:
29 }
30
31 wg.Add(1)
32 go func(url string) {
33 defer wg.Done()
34
35 limiter <- struct{}{} // 获取令牌
36 defer func() { <-limiter }() // 释放令牌
37
38 start := time.Now()
39 resp, err := http.Get(url)
40 d := time.Since(start)
41
42 mu.Lock()
43 defer mu.Unlock()
44
45 r := Result{URL: url, Duration: d}
46 if err != nil {
47 r.Err = err
48 } else {
49 r.StatusCode = resp.StatusCode
50 resp.Body.Close()
51 }
52 results = append(results, r)
53 }(u)
54 }
55
56 wg.Wait()
57 return results
58}
实战二:Pipeline 模式 — 数据流的流水线处理
将处理流程拆分为多个 stage,每个 stage 由一组 goroutine 处理:

1// Stage 1: 生成数字流
2func generator(ctx context.Context, nums ...int) <-chan int {
3 out := make(chan int)
4 go func() {
5 defer close(out)
6 for _, n := range nums {
7 select {
8 case out <- n:
9 case <-ctx.Done():
10 return
11 }
12 }
13 }()
14 return out
15}
16
17// Stage 2: 平方运算
18func square(ctx context.Context, in <-chan int) <-chan int {
19 out := make(chan int)
20 go func() {
21 defer close(out)
22 for n := range in {
23 select {
24 case out <- n * n:
25 case <-ctx.Done():
26 return
27 }
28 }
29 }()
30 return out
31}
32
33// Stage 3: 过滤结果
34func filter(ctx context.Context, in <-chan int, predicate func(int) bool) <-chan int {
35 out := make(chan int)
36 go func() {
37 defer close(out)
38 for n := range in {
39 if predicate(n) {
40 select {
41 case out <- n:
42 case <-ctx.Done():
43 return
44 }
45 }
46 }
47 }()
48 return out
49}
Pipeline 的优雅之处在于:当你取消 ctx 时,整个流水线会逐级关闭,不会留下泄漏的 goroutine。
并发模型对比
| 模型 | 代表语言 | 调度方式 | 栈大小 | 通信机制 |
|---|---|---|---|---|
| 1:1 线程 | C, Java (传统) | OS 内核 | ~8 MB | 共享内存 + 锁 |
| M:N 协程 | Go | 用户态 GMP | ~2 KB (可增长) | Channel |
| async/await | Rust, JS, Python | 编译器状态机 | 零开销 | Future/Promise |
| Actor 模型 | Erlang/Elixir | BEAM VM | ~2.5 KB | 消息传递 |
| Structured Concurrency | Kotlin, Swift | 语言级作用域 | — | 结构化作用域 |
性能数据
以下是在 8 核机器上对比不同并发模式的吞吐量:
| 场景 | 模式 | QPS | P99 延迟 | goroutine 数量 |
|---|---|---|---|---|
| HTTP Proxy | Goroutine-per-conn | 45,000 | 12 ms | ~200 |
| 数据聚合 | Fan-in / Fan-out | 120,000 | 3 ms | ~80 |
| 日志写入 | 有缓冲 channel + 单 writer | 480,000 | 0.5 ms | 2 |
| RPC 网关 | Worker Pool (GOMAXPROCS) | 85,000 | 8 ms | ~32 |
常见陷阱与解法
1. Goroutine 泄漏
1// ❌ BAD: 如果 ctx 被取消,ch 永远不会被读取,goroutine 永久阻塞
2func bad(ctx context.Context) <-chan int {
3 ch := make(chan int)
4 go func() {
5 result := expensiveComputation()
6 ch <- result // ctx 取消后无人接收,goroutine 泄漏!
7 }()
8 return ch
9}
10
11// ✓ GOOD: 使用 select 响应取消
12func good(ctx context.Context) <-chan int {
13 ch := make(chan int)
14 go func() {
15 result := expensiveComputation()
16 select {
17 case ch <- result:
18 case <-ctx.Done(): // 安全退出
19 return
20 }
21 }()
22 return ch
23}
2. 关闭已关闭的 Channel
1// panic: close of closed channel
2func safeClose() {
3 ch := make(chan int)
4 var once sync.Once
5 closeCh := func() { once.Do(func() { close(ch) }) }
6
7 go func() { closeCh() }()
8 go func() { closeCh() }() // 使用 sync.Once 保证只关一次
9}
3. for range 循环变量捕获
1// ❌ BAD (Go < 1.22): 循环变量在迭代间复用
2for _, v := range items {
3 go func() {
4 process(v) // v 被所有 goroutine 共享!
5 }()
6}
7
8// ✓ GOOD: 将变量作为参数传入
9for _, v := range items {
10 go func(val Item) {
11 process(val)
12 }(v)
13}
14// 或 Go ≥ 1.22 中循环变量自动具有每次迭代独立的作用域
小结
通过本文,我们系统地梳理了 Go 并发的核心范式和常见模式:
| 知识点 | 关键内容 |
|---|---|
| 并发原语 | goroutine (轻量线程), channel (类型安全管道), sync 包 (WaitGroup/Mutex/Once) |
| Context 传递 | context.WithTimeout, context.WithCancel 构建可取消的调用链 |
| Pipeline | 多 stage <-chan 串联,取消信号沿流水线传播 |
| Fan-in / Fan-out | 多路扇入、多工作协程扇出,select 语句实现多路复用 |
| 常见陷阱 | goroutine 泄漏、channel panic、循环变量捕获 |
Go 的并发模型将 CSP(Communicating Sequential Processes)的思想带入了主流工程实践——用 channel 连接 goroutine,就是并发版的 Unix 管道。