6.7 KiB
tags, create time
| tags | create time | |||||
|---|---|---|---|---|---|---|
|
2026-06-07 14:40 |
Channel
概述
Channel 是 Go 中 goroutine 之间通信的核心机制。Go 倡导"以通信共享内存,而非共享内存来通信",channel 正是这一哲学(CSP 模型)的具象化体现。本文涵盖 channel 的创建、操作、单向/双向类型、缓冲语义以及实战技巧。
正文
什么是 Channel?
[!question] 💭 思考 如果多个 goroutine 需要交换数据,除了用锁保护共享变量,还有没有更安全的方式?
官方定义:Channel 是一个类型化的管道,通过它你可以发送和接收值。
ch := make(chan int) // 创建一个传递 int 的 channel
ch <- 42 // 发送:向 ch 写入 42
v := <-ch // 接收:从 ch 读取值到 v
[!info] ℹ️ 核心理念 "Don't communicate by sharing memory; share memory by communicating." — Rob Pike 与其用锁保护共享数据,不如让数据在 goroutine 之间流动——拥有数据的 goroutine 就是唯一能修改它的那个。
创建与初始化
// 无缓冲 channel(同步模式)
ch := make(chan int)
// 有缓冲 channel(异步模式,容量为 3)
ch := make(chan int, 3)
[!warning] ⚠️ 常见错误
- 未初始化的 channel 是
nil,对 nil channel 发送/接收会永久阻塞- 重复关闭 channel 会 panic
- 向已关闭的 channel 发送会 panic
基本操作
| 操作 | 语法 | 说明 |
|---|---|---|
| 发送 | ch <- value |
向 channel 写入,可能阻塞 |
| 接收 | <-ch |
从 channel 读取,可能阻塞 |
| 带值接收 | v := <-ch |
读取值并赋值 |
| 关闭 | close(ch) |
标记 channel 不再发送数据 |
package main
import "fmt"
func main() {
ch := make(chan int)
go func() {
ch <- 42 // 生产者:发送数据
close(ch) // 生产完毕,关闭 channel
}()
v := <-ch // 消费者:接收数据
fmt.Println(v) // 42
}
关闭 Channel 后的行为
[!question] 💭 思考 关闭一个 channel 后还能读吗?还能写吗?读完已有数据后再读会怎样?
| 操作 | 未关闭 | 已关闭(有数据) | 已关闭(空) |
|---|---|---|---|
| 发送 | ✅ 正常 | ❌ panic | ❌ panic |
| 接收 | ✅ 正常 | ✅ 返回已有数据 | ✅ 返回零值 |
| 关闭 | ✅ 正常 | ✅ 可重复关闭检查 | ❌ panic(重复关闭) |
ch := make(chan int, 5)
ch <- 1
close(ch)
// 关闭后仍可读取
for i := 0; i < 5; i++ {
v := <-ch
fmt.Println(v) // 第1次: 1, 之后全是 0
}
安全读取:ok 模式
当需要从已关闭的 channel 区分"有数据"和"已关闭"时,使用多返回值形式:
v, ok := <-ch
if ok {
fmt.Println("收到数据:", v)
} else {
fmt.Println("channel 已关闭,无更多数据")
}
for-range 读取
这是最优雅的 channel 消费方式——自动在 channel 关闭时退出循环:
go func() {
for v := range ch { // channel 关闭后自动退出
fmt.Println("收到:", v)
}
fmt.Println("channel 已关闭,循环结束")
}()
close(ch) // 触发 for-range 退出
[!tip] 💡 最佳实践 发送端负责关闭,接收端不负责关闭。如果一个 channel 有多个发送方,谁该关闭它?这种模糊场景应尽量避免。
有缓冲 vs 无缓冲
flowchart LR
subgraph Unbuf["无缓冲 channel"]
S[发送方] -->|"阻塞直到接收方就绪"| R[接收方]
end
subgraph Buf["有缓冲 channel (cap=3)"]
B["缓冲区 📦📦"] -.->|剩余容量| S2[发送方]
R2[接收方] -.->|有空位| S2
R2 -->|"取出"| B
end
- 无缓冲 channel:发送和接收同步发生——发送方阻塞直到接收方准备好,反之亦然。适用于需要严格配对的生产者-消费者场景。
- 有缓冲 channel:发送方在缓冲区未满时无需等待即可返回;接收方在缓冲区非空时无需等待即可拿到数据。适用于解耦生产速率和消费速率的场景。
[!warning] ⚠️ 注意 缓冲区满了以后,有缓冲 channel 也会退化为同步模式——发送方将被阻塞。长期满队列意味着消费者跟不上生产者的节奏,应考虑增加消费者数量或扩大缓冲区。
单向 Channel
有时我们希望限制 channel 的使用方向,比如在函数签名中明确表达"这个参数只用于发送"或"只用于接收":
// 只发送 channel —— 只能 ch<-value
sendCh := make(chan int)
var sendOnly chan<- int = sendCh
// 只接收 channel —— 只能 <-ch
var recvOnly <-chan int = sendCh
func producer(out chan<- int) {
for i := 0; i < 5; i++ {
out <- i // 只能发,不能收
}
close(out)
}
func consumer(in <-chan int) {
for v := range in { // 只能收,不能发
fmt.Println("received:", v)
}
}
[!note] 📝 关键规则 只有接收方才能关闭 channel。单向 channel 的类型转换是单向的:双向可以转为单向,单向不能转回双向。
经典模式:扇入扇出
[!question] 💭 思考 如果有 10 个任务要并行处理,但结果需要汇总到一个地方,怎么设计?
func worker(id int, jobs <-chan int, results chan<- int) {
for j := range jobs {
fmt.Printf("worker %d processing job %d\n", id, j)
results <- j * 2
}
}
func main() {
jobs := make(chan int, 100)
results := make(chan int, 100)
// 启动 3 个 worker(扇出)
for w := 1; w <= 3; w++ {
go worker(w, jobs, results)
}
// 发送任务
for j := 1; j <= 9; j++ {
jobs <- j
}
close(jobs)
// 等待所有 worker 完成
close(results)
for r := range results {
fmt.Println("result:", r)
}
}
Channel 实现互斥锁
[!question] 💭 思考 channel 本身已经是并发安全的了,能不能利用这一点来代替 mutex?
一个容量为 1 的 channel 可以充当信号量:
ch := make(chan struct{}, 1)
ch <- struct{}{} // 获取"锁"
// 临界区:同时只有一个 goroutine 能执行
*counter++
<-ch // 释放"锁"
[!tip] 💡 何时用 Channel vs Mutex?
- 优先用 channel:goroutine 间的数据传递、事件通知、生命周期管理
- 优先用 mutex:保护共享变量的并发访问
- 两者不互斥,复杂场景中经常配合使用