Go 从 0 到精通 · 第 26 课:channel 入门
Go 从 0 到精通 · 第 26 课:channel 入门
学习定位:这是整套 Go 教程的第 26 课,也是阶段五(并发与工程阶段)的第二课。
前置要求:已经完成第 25 课,理解 goroutine 的创建和 WaitGroup 的使用。
本课目标:理解 channel 的概念和工作机制,掌握无缓冲 channel 和有缓冲 channel 的区别,能用 channel 在 goroutine 之间安全地传递数据,理解 channel 的阻塞行为和关闭机制。
1. 本课你要解决的核心问题
上一课你学会了用 goroutine 并行执行任务。但有一个问题没解决:goroutine 之间怎么通信?
上一课我们用"预分配切片 + 按索引写入"来收集结果,这在简单场景下可以。但如果需求变复杂一点——
- 一个 goroutine 产生数据,另一个消费数据
- 多个 goroutine 协作完成一个流水线任务
- 一个 goroutine 通知另一个"可以开始了"
这些都需要 goroutine 之间能安全地传递数据。Go 给出的答案就是 channel。
Go 有一句著名的并发哲学:
“不要通过共享内存来通信,而要通过通信来共享内存。”
channel 就是这句话的具体实现。
你需要搞明白以下问题:
- channel 是什么
- 怎么创建 channel,怎么发送和接收数据
- 无缓冲 channel 和有缓冲 channel 有什么区别
- channel 什么时候会阻塞
- 怎么关闭 channel,关闭后会怎样
- 怎么用
range遍历 channel - 什么是单向 channel
学完这一课,你就掌握了 Go 并发编程最核心的通信工具。
2. channel 是什么
2.1 一句话定义
channel 是 goroutine 之间传递数据的管道。
你可以把 channel 想象成一根管子:一头塞东西进去(发送),另一头把东西拿出来(接收)。
1 | goroutine A ──发送──▶ [ channel ] ──接收──▶ goroutine B |
2.2 channel 的类型
channel 是有类型的。一个 chan int 只能传 int,一个 chan string 只能传 string:
1 | var ch1 chan int // 传 int 的 channel |
2.3 channel 的零值
channel 的零值是 nil。对 nil channel 发送或接收会永久阻塞:
1 | var ch chan int // nil |
所以 channel 必须用 make 创建后才能使用。
3. 创建 channel
3.1 无缓冲 channel
1 | ch := make(chan int) // 创建一个无缓冲的 int channel |
无缓冲 channel 没有任何存储空间。发送方发送数据后必须等接收方来取,接收方想取数据也必须等发送方来送。两方必须同时到位,数据才能传递。
就像面对面交接物品:你必须等对方伸手才能把东西给出去。
3.2 有缓冲 channel
1 | ch := make(chan int, 5) // 创建一个容量为 5 的有缓冲 channel |
有缓冲 channel 有一个内部队列,可以暂存数据。发送方可以往里塞数据,只要队列没满就不会阻塞。接收方从队列中取数据,只要队列不空就不会阻塞。
就像一个快递柜:寄件人放进去就走,取件人有空再来取。柜子放满了,寄件人才需要等。
4. 发送和接收
4.1 操作符 <-
channel 用 <- 操作符发送和接收数据:
1 | ch <- value // 发送:把 value 发送到 ch |
箭头方向就是数据流动方向:ch <- value 是数据流入 channel,<-ch 是数据从 channel 流出。
4.2 第一个 channel 示例
1 | package main |
执行流程:
- 创建 channel
- 启动一个 goroutine,它尝试往 channel 发送数据
- 主 goroutine 执行
<-ch,阻塞等待数据 - 新 goroutine 发送
"你好,channel!" - 主 goroutine 收到数据,解除阻塞,打印结果
注意:这里不需要 WaitGroup。<-ch 本身就是一种等待机制——主 goroutine 会一直阻塞,直到收到数据。channel 天然地起到了同步的作用。
4.3 用 channel 替代 WaitGroup
1 | package main |
done <- true 就是 worker 在说"我干完了",<-done 就是主 goroutine 在等这句话。
5. 无缓冲 channel 的阻塞行为
5.1 发送阻塞,直到有人接收
1 | package main |
运行这段代码,你会得到一个**死锁(deadlock)**错误:
1 | fatal error: all goroutines are asleep - deadlock! |
因为发送操作 ch <- 42 需要另一个 goroutine 来接收,但只有一个主 goroutine,没人来接收,所以永远阻塞。Go 运行时检测到所有 goroutine 都在等待,就报死锁。
5.2 接收阻塞,直到有人发送
1 | package main |
同样是死锁。接收操作需要有人发送数据。
5.3 无缓冲 channel 是同步的
1 | package main |
输出:
1 | main: 等待接收... |
无缓冲 channel 的核心特性:发送和接收必须同时就绪。这让它成为 goroutine 之间最简单的同步工具。
6. 有缓冲 channel
6.1 基本用法
1 | package main |
有缓冲 channel 像一个队列:先进先出(FIFO)。
6.2 阻塞规则
1 | 发送:缓冲区满了才阻塞 |
1 | ch := make(chan int, 2) |
6.3 len 和 cap
1 | ch := make(chan int, 5) |
6.4 无缓冲 vs 有缓冲
| 特性 | 无缓冲 make(chan T) |
有缓冲 make(chan T, n) |
|---|---|---|
| 容量 | 0 | n |
| 发送阻塞 | 直到有人接收 | 直到缓冲区满 |
| 接收阻塞 | 直到有人发送 | 直到缓冲区空 |
| 同步性 | 强同步(握手) | 弱同步(排队) |
| 类比 | 面对面交接 | 快递柜 |
怎么选:
- 需要严格同步(确保一方完成后另一方才继续)→ 无缓冲
- 允许发送方先跑一段,不必等接收方 → 有缓冲
- 不确定用哪个 → 先用无缓冲,需要优化时再加缓冲
7. 关闭 channel
7.1 为什么要关闭
当发送方不再发送数据时,需要告诉接收方"没有更多数据了"。这就是关闭 channel 的作用。
7.2 close 函数
1 | ch := make(chan int, 3) |
7.3 关闭后的行为
1 | package main |
关闭后的规则:
| 操作 | 结果 |
|---|---|
| 接收(缓冲区还有数据) | 正常返回数据 |
| 接收(缓冲区空了) | 返回零值,不阻塞 |
| 发送 | panic! |
| 再次关闭 | panic! |
7.4 判断 channel 是否已关闭
接收操作可以返回两个值:
1 | v, ok := <-ch |
ok == true:收到了正常的数据ok == false:channel 已关闭且缓冲区为空
1 | package main |
7.5 谁来关闭 channel
原则:只有发送方关闭 channel,接收方不要关闭。
因为发送方知道自己什么时候不再发送了。如果接收方关闭了 channel,发送方继续发送就会 panic。
1 | // 正确:发送方关闭 |
8. range 遍历 channel
8.1 基本用法
for range 可以遍历 channel,它会一直接收数据,直到 channel 被关闭:
1 | package main |
输出:
1 | 1 |
8.2 如果不关闭 channel
1 | go func() { |
for range 遍历 channel 时,必须有人关闭 channel,否则 range 永远不会结束。
8.3 多个发送者的情况
如果有多个 goroutine 往同一个 channel 发送数据,什么时候关闭?
1 | package main |
技巧:用 WaitGroup 等待所有发送者完成,然后在一个单独的 goroutine 中关闭 channel。这样 range 就能正常结束了。
9. 单向 channel
9.1 为什么需要单向 channel
有时候你希望一个函数只能往 channel 发送数据,另一个函数只能从 channel 接收数据。这样可以防止误操作。
9.2 语法
1 | chan<- int // 只能发送的 channel(send-only) |
记忆方法:箭头指向 chan 就是发送,箭头从 chan 出来就是接收。
9.3 用在函数参数中
1 | package main |
双向 channel 可以隐式转换为单向 channel,但单向不能转回双向。这是一种类型安全机制:
1 | ch := make(chan int) |
9.4 单向 channel 的好处
1 | // 如果 producer 不小心写了 <-ch,编译器直接报错 |
编译器帮你检查,防止在不该接收的地方接收、不该发送的地方发送。
10. 实战示例
10.1 生产者-消费者模式
这是并发编程中最经典的模式:一方生产数据,另一方消费数据。
1 | package main |
可能的输出:
1 | 生产: 1 |
观察:生产者比消费者快,但缓冲区只有 3,所以生产者会被限速。这就是 channel 的背压(backpressure)机制——消费者处理不过来时,生产者会自动慢下来。
10.2 用 channel 收集 goroutine 的结果
1 | package main |
这比上一课用"预分配切片 + 按索引写入"更优雅:
- 结果按完成顺序返回,先完成的先输出
- 不需要预先知道有多少结果
- channel 保证了并发安全
10.3 流水线(Pipeline)
channel 可以把多个处理步骤串起来,形成数据处理流水线:
1 | package main |
输出:
1 | 16 |
流水线的每一步都在独立的 goroutine 中运行,通过 channel 连接。数据像水一样从一个阶段流向下一个阶段。
函数返回 <-chan int(只读 channel) 是 Go 中流水线的标准写法:每个阶段负责创建 channel、启动 goroutine、返回只读 channel。调用方只管从返回的 channel 中读取。
10.4 用 channel 做信号通知
有时候 channel 不传具体数据,只用来传递"某件事发生了"的信号:
1 | package main |
chan struct{} 是信号通知的惯用类型。struct{} 不占任何内存(大小为 0),表示"我不关心传什么值,只关心有没有信号"。
用 close(done) 而不是 done <- struct{}{},好处是:关闭 channel 后,所有在 <-done 上等待的 goroutine 都会被唤醒。这是一种广播机制。
11. 常见坑总结
11.1 往已关闭的 channel 发送数据——panic
1 | ch := make(chan int) |
这是最常见的 channel 错误。记住:关闭 channel 是发送方的事,接收方永远不要关闭。
11.2 关闭已关闭的 channel——panic
1 | ch := make(chan int) |
不要重复关闭 channel。
11.3 死锁——所有 goroutine 都在等待
1 | func main() { |
无缓冲 channel 在同一个 goroutine 中既发送又接收,必定死锁。
1 | // 有缓冲的 channel 可以在同一个 goroutine 中操作(只要不超过缓冲区) |
11.4 忘记关闭 channel 导致 range 卡死
1 | func main() { |
for range 会一直等到 channel 关闭才停止。不关闭就会变成死锁。
11.5 只发不收——goroutine 泄漏
1 | func main() { |
每个往 channel 发送的数据都要有人接收。如果没人收,发送方的 goroutine 会永远阻塞。
11.6 nil channel 的用途
1 | var ch chan int // nil |
nil channel 看起来没用,但在 select(下一课)中有特殊用途——可以动态禁用某个 case。
11.7 缓冲区大小不是越大越好
1 | ch := make(chan int, 1000000) // 不要随便设一百万 |
缓冲区太大会掩盖问题——如果消费者比生产者慢,大缓冲区只是延迟了阻塞,内存会越积越多。合理的缓冲区大小通常是个位数到几十。
12. 本课练习
练习 1:乒乓球
要求:
- 两个 goroutine 模拟乒乓球对打
- 用一个 channel 传递"球"(一个 int,代表击球次数)
- 每次接到球后次数加 1,打印
"Player X 击球 N 次"后把球传回去 - 打到 10 次后结束
练习 2:扇出(Fan-out)
要求:
- 一个生产者 goroutine 生成 1~20 的数字,发送到 channel
- 启动 3 个消费者 goroutine 从同一个 channel 接收并打印
- 观察哪个消费者接收了哪些数字
提示:多个 goroutine 从同一个 channel 接收,每条数据只会被一个 goroutine 拿到。
练习 3:求和流水线
要求:
- 第一阶段:生成 1~100 的数字
- 第二阶段:过滤出偶数
- 第三阶段:累加所有偶数
- 每个阶段一个 goroutine,用 channel 连接
- 最后输出结果(应该是 2550)
练习 4:超时处理(预习)
要求:
- 启动一个 goroutine,模拟一个可能很慢的操作(随机 1~5 秒)
- 主 goroutine 最多等 2 秒
- 如果 2 秒内拿到结果就打印,否则打印"超时"
提示:用 time.After(2 * time.Second) 和 select(这是下一课的内容,先尝试自己查资料实现)。
练习 5:安全计数器
要求:
- 启动 100 个 goroutine,每个 goroutine 往 channel 发送 1
- 主 goroutine 从 channel 接收 100 个值并累加
- 输出最终结果(应该是 100)
13. 自测题
13.1 概念题
- channel 的零值是什么?对 nil channel 发送会怎样?
- 无缓冲 channel 和有缓冲 channel 的阻塞规则分别是什么?
- 关闭 channel 后,发送和接收分别会怎样?
v, ok := <-ch中ok为false代表什么?- 为什么只有发送方应该关闭 channel?
for range ch什么时候结束?chan struct{}是什么意思?什么时候用?chan<- int和<-chan int分别是什么?
13.2 代码阅读题
预测以下代码的输出:
1 | package main |
点击查看答案
1 | 1 |
解释:
ch <- 1:缓冲区 [1]ch <- 2:缓冲区 [1, 2]<-ch:取出 1,缓冲区 [2],打印 1ch <- 3:缓冲区 [2, 3](不会阻塞,因为刚取了一个)<-ch:取出 2,打印 2<-ch:取出 3,打印 3
channel 是 FIFO(先进先出),所以输出顺序就是发送顺序。
14. 本课总结
这一课你学到了 Go 并发编程最核心的通信机制——channel。
| 场景 | 做法 |
|---|---|
| 创建 channel | make(chan T) 或 make(chan T, n) |
| 发送 / 接收 | ch <- v / v := <-ch |
| 关闭 channel | close(ch)(发送方关闭) |
| 遍历 channel | for v := range ch(需要关闭才能结束) |
| 判断是否关闭 | v, ok := <-ch,ok == false 表示已关闭 |
| 信号通知 | chan struct{},用 close 广播 |
| 限制方向 | chan<- T(只发送)、<-chan T(只接收) |
最重要的三件事:
- channel 是 goroutine 之间传递数据的管道——发送和接收是并发安全的
- 无缓冲 channel 是同步的,有缓冲 channel 有队列——按需选择
- 只有发送方关闭 channel——往已关闭的 channel 发送会 panic
15. 下一课预告
这一课你学会了用 channel 在两个 goroutine 之间传递数据。但如果一个 goroutine 需要同时监听多个 channel呢?
- 同时等待用户输入和超时信号
- 同时等待多个服务的响应
- 处理"哪个先来就先处理哪个"的场景
下一课:select 与并发控制
会重点讲:
select是什么——channel 的多路复用器- 怎么用
select同时监听多个 channel - 超时处理——
time.After配合select default分支——非阻塞操作- 常见的
select使用模式
学完下一课,你就能处理更复杂的并发场景了。





