Goroutine、Channel 和 Context
在 Go 语言中,Goroutine、Channel 和 Context 是并发编程的“三驾马车”。它们各司其职,组合在一起构成了 Go 强大的并发模型。
用一个简单的比喻来理解:如果你在经营一家快递公司,Goroutine 就是搬砖的快递员,Channel 就是传输货物的传送带,而 Context 则是经理发出的各种控制指令和通知。
1. Goroutine:并发的执行体(干活的人)
Goroutine 是 Go 运行时(Runtime)管理的轻量级线程。
-
职责:只负责执行代码。你让它做什么,它就异步地去执行什么。
-
特点:极其轻量。创建一个 Goroutine 仅需几 KB 的栈内存,默认由 Go 调度器(M:N 调度模型)自动分配到物理线程上运行,所以你可以轻松轻松启动成千上万个 Goroutine。
-
局限:它是一个“盲目”的执行者。启动之后,它和外界是隔离的,不知道其他 Goroutine 的状态,也无法直接安全地把数据传给别人。
2. Channel:并发的通信机制(沟通的桥梁)
Channel 是用来在多个 Goroutine 之间传递数据和同步状态的管道。
-
职责:解决 Goroutine 之间的通信问题。Go 提倡 “不要通过共享内存来通信,而要通过通信来共享内存”,Channel 就是这个理念的落地实现。
-
特点:它是类型安全且线程安全(内置锁机制)的。多个 Goroutine 同时向同一个 Channel 读写数据,不会发生数据竞态(Data Race)。
-
作用:除了传数据,Channel 还能用作同步信号(比如一个无缓冲的 Channel 或
chan struct{}可以用来等待某个任务完成)。
3. Context:并发的上下文控制(指挥官)
Context 专门用于在 Goroutine 树状生命周期中传递取消信号、超时控制以及元数据(Key-Value)。
-
职责:解决 Goroutine 的控制和退出问题。
-
特点:级联传播。如果一个根 Context 被取消或超时,所有由它派生出来的子 Context 都会收到取消信号,从而通知对应的 Goroutine 优雅退出。
-
典型场景:当一个 HTTP 请求进来,你启动了 5 个 Goroutine 去分别查数据库、调 API、写日志。如果客户端中途断开连接,或者整个请求超时了,通过 Context 就能瞬间通知这 5 个 Goroutine 赶紧停手,释放资源,防止 Goroutine 泄露。
三者的核心区别与对比
| 特性 | Goroutine | Channel | Context |
|---|---|---|---|
| 角色 | 执行者 (Worker) | 传输者 (Conveyor) | 指挥官 (Controller) |
| 核心目的 | 实现异步和并发执行 | 实现数据的安全传递与协作 | 实现生命周期控制、超时与追踪 |
| 关注点 | “怎么让这段代码并发运行?” | “数据怎么从 A 传到 B?” | “这个任务什么时候该停下来?” |
| 数据流向 | 无流向(仅执行) | 双向或单向的数据流动 | 严格的自上而下(从父到子)单向传递 |
它们是如何协同工作的?
在实际开发中,这三者通常是组合出现的。下面是一个经典的组合使用场景:
Go
package main
import (
"context"
"fmt"
"time"
)
func worker(ctx context.Context, id int, dataChan chan int) {
for {
select {
case <-ctx.Done():
// Context 发出退出指令(超时或手动取消)
fmt.Printf("工人 %d:收到退出指令,停止干活。\n", id)
return
case val, ok := <-dataChan:
if !ok {
fmt.Printf("工人 %d:传送带关闭了,下班。\n", id)
return
}
// Channel 传递数据
fmt.Printf("工人 %d:正在处理数据 %d\n", id, val)
time.Sleep(500 * time.Millisecond)
}
}
}
func main() {
// 1. 创建一个 1.2 秒后自动超时的 Context (指挥官)
ctx, cancel := context.WithTimeout(context.Background(), 1200*time.Millisecond)
defer cancel()
// 2. 创建一个 Channel (传送带)
ch := make(chan int, 10)
// 3. 启动 2. 个 Goroutine (快递员)
go worker(ctx, 1, ch)
go worker(ctx, 2, ch)
// 主 Goroutine 往 Channel 发送数据
for i := 1; i <= 5; i++ {
ch <- i
}
// 阻塞等待 Context 超时
<-ctx.Done()
fmt.Println("主程序:时间到了,结束。")
}
-
Goroutine (
go worker) 让任务动起来。 -
Channel (
ch) 把主线程的数据源源不断地送给各个 worker。 -
Context (
ctx) 在 1.2 秒超时一到,瞬间让所有 worker 安全、优雅地停下。