Go: Go Channel 通信
Channel 是 Go 并发模型的核心原语——"不要通过共享内存来通信,而应通过通信来共享内存。"channel 让 goroutine 之间的数据传递像管道一样安全优雅。
如果说 goroutine 是 Go 并发的"人",channel 就是他们之间的"电话线"。这节课你将掌握 channel 的全部核心用法和常见陷阱。
1. 你将学到
- channel 创建(
make(chan T)无缓冲 /make(chan T, n)有缓冲) - 发送
ch <- v与接收v := <-ch - 无缓冲 channel 的同步行为
- 有缓冲 channel 的异步行为
close关闭 channel 与rangeiterate- 方向限制:
chan<-只写 /<-chan只读 - 常见死锁场景分析
- select 多路复用入门
- 生产者-消费者管道综合案例
2. 一个后端工程师的真实故事
(1) 痛点:共享内存 + Mutex 代码又丑又容易死锁
Alice 是支付团队的后端工程师,她需要在多个 goroutine 之间传递交易数据:
"我用
sync.Mutex保护一个共享的[]Transactionslice,5 个 goroutine 往里写、3 个 goroutine 往外读。代码 100 行里 30 行是 Lock/Unlock,reviewer 看了直接说'这代码我 review 不了一行都不敢动'。"
她的代码像这样:
// 共享内存方式:读写都加锁,代码难以维护
type TransactionPool struct {
mu sync.Mutex
items []Transaction
}
func (p *TransactionPool) Add(t Transaction) {
p.mu.Lock()
defer p.mu.Unlock()
p.items = append(p.items, t)
}
func (p *TransactionPool) Get() Transaction {
p.mu.Lock()
defer p.mu.Unlock()
if len(p.items) == 0 {
return Transaction{}
}
item := p.items[0]
p.items = p.items[1:]
return item
}
(2) Go 的解法:channel 传递数据
// channel_approach.go
package main
import "fmt"
type Transaction struct {
ID string
Amount float64
}
func main() {
// 创建一个无缓冲 channel
ch := make(chan Transaction)
// 生产者 goroutine
go func() {
tx := Transaction{ID: "TXN-001", Amount: 99.99}
ch <- tx // 发送(阻塞直到接收方就绪)
fmt.Println("生产者: 发送完成")
}()
// 消费者
tx := <-ch // 接收(阻塞直到发送方就绪)
fmt.Printf("消费者: 收到 %s ($%.2f)\n", tx.ID, tx.Amount)
}
输出:
消费者: 收到 TXN-001 ($99.99)
生产者: 发送完成
(3) 收益:channel vs 共享内存
| 维度 | 共享内存 + Mutex | Channel |
|---|---|---|
| 代码量 | 30 行模板代码 | 1 行 ch <- v |
| 线程安全 | 手动 Lock/Unlock | 语言内置保证 |
| 耦合度 | 高(共享变量耦合) | 低(只依赖 chan interface) |
| 可测试性 | 需要 mock Mutex | 直接用 channel 测试 |
| 死锁风险 | 容易(Lock 顺序问题) | 编译期可发现部分死锁 |
3. channel 创建与基本操作
(1) 创建 channel
package main
import "fmt"
func main() {
// 无缓冲 channel(同步)
ch1 := make(chan int)
// 有缓冲 channel(异步,容量 3)
ch2 := make(chan string, 3)
// nil channel(不能直接使用)
var ch3 chan float64
fmt.Printf("ch1: %T, 无缓冲\n", ch1)
fmt.Printf("ch2: %T, 缓冲=%d\n", ch2, cap(ch2))
fmt.Printf("ch3: %T, nil=%v\n", ch3, ch3 == nil)
}
(2) 发送与接收
ch := make(chan int)
// 发送:ch <- value
ch <- 42 // 无缓冲:阻塞直到有人接收
// 接收:value := <-ch
value := <-ch // 阻塞直到有人发送
// 忽略接收值
<-ch // 只接收不处理(用于同步信号)
▶ 示例:无缓冲 channel 的同步行为
package main
import (
"fmt"
"time"
)
func main() {
ch := make(chan string)
go func() {
fmt.Println("goroutine: 准备发送...")
ch <- "hello" // 阻塞直到 main 接收
fmt.Println("goroutine: 发送完成")
}()
time.Sleep(1 * time.Second) // 模拟延迟
fmt.Println("main: 准备接收...")
msg := <-ch
fmt.Printf("main: 收到 %q\n", msg)
}
输出:
goroutine: 准备发送...
main: 准备接收...
goroutine: 发送完成
main: 收到 "hello"
4. 无缓冲 vs 有缓冲 channel
(1) 有缓冲 channel 的异步行为
package main
import (
"fmt"
"time"
)
func main() {
ch := make(chan int, 3) // 容量 3
go func() {
for i := 1; i <= 5; i++ {
ch <- i
fmt.Printf("发送 %d (len=%d)\n", i, len(ch))
}
close(ch)
}()
time.Sleep(500 * time.Millisecond)
for v := range ch {
fmt.Printf("接收 %d\n", v)
time.Sleep(200 * time.Millisecond)
}
}
输出:
发送 1 (len=0)
发送 2 (len=1)
发送 3 (len=2)
发送 4 (len=3)
发送 5 (len=4)
接收 1
接收 2
接收 3
接收 4
接收 5
▶ 示例:缓冲满时阻塞
package main
import (
"fmt"
"time"
)
func main() {
ch := make(chan int, 2) // 容量 2
ch <- 1 // ✅ 不阻塞(有空间)
ch <- 2 // ✅ 不阻塞(有空间)
// ch <- 3 // ❌ 阻塞!缓冲满了,等待接收方
go func() {
time.Sleep(100 * time.Millisecond)
fmt.Println("goroutine: 开始接收")
<-ch // 释放一个位置
}()
fmt.Println("main: 发送 3")
ch <- 3 // 现在不阻塞了(goroutine 已接收了一个)
fmt.Println("main: 发送完成")
close(ch)
for v := range ch {
fmt.Printf("v=%d\n", v)
}
}
(2) 无缓冲 vs 有缓冲对比
| 特性 | 无缓冲 make(chan T) |
有缓冲 make(chan T, n) |
|---|---|---|
| 发送行为 | 阻塞直到接收方就绪 | 缓冲有空位时不阻塞 |
| 接收行为 | 阻塞直到发送方就绪 | 缓冲有数据时不阻塞 |
| 同步/async | 同步(握手) | async(队列) |
| 容量 | 0 | n |
| 典型场景 | 同步信号、goroutine 间协调 | 任务队列、流水线 |
5. close 与 range iterate
(1) close 关闭 channel
package main
import "fmt"
func main() {
ch := make(chan int, 3)
ch <- 1
ch <- 2
close(ch) // 关闭后不能再发送
// 关闭后可以继续接收剩余数据
v1 := <-ch // 1
v2 := <-ch // 2
v3 := <-ch // 0(零值,ok=false)
fmt.Println(v1, v2, v3)
}
▶ 示例:comma-ok 判断 channel 是否关闭
package main
import "fmt"
func main() {
ch := make(chan int, 2)
ch <- 10
ch <- 20
close(ch)
// comma-ok:ok=false 表示 channel 已关闭且无数据
v, ok := <-ch
fmt.Printf("v=%d, ok=%v\n", v, ok) // v=10, ok=true
v, ok = <-ch
fmt.Printf("v=%d, ok=%v\n", v, ok) // v=20, ok=true
v, ok = <-ch
fmt.Printf("v=%d, ok=%v\n", v, ok) // v=0, ok=false(已关闭且无数据)
}
(2) for range 遍历 channel
package main
import "fmt"
func main() {
ch := make(chan string, 3)
ch <- "Alice"
ch <- "Bob"
ch <- "Charlie"
close(ch)
// for range 自动在 channel 关闭后退出
for name := range ch {
fmt.Printf("Hello, %s!\n", name)
}
// 等价于:
// for {
// name, ok := <-ch
// if !ok { break }
// fmt.Println(name)
// }
}
for range 自动处理。永远由发送方负责 close。
6. 方向限制
(1) 函数参数指定 channel 方向
package main
import "fmt"
// 只写 channel(只能发送)
func producer(out chan<- int) {
for i := 1; i <= 3; i++ {
out <- i // ✅ 只能发送
}
close(out)
}
// 只读 channel(只能接收)
func consumer(in <-chan int) {
for v := range in {
fmt.Printf("收到: %d\n", v) // ✅ 只能接收
}
}
func main() {
ch := make(chan int, 3)
go producer(ch) // 自动转为 chan<- int
consumer(ch) // 自动转为 <-chan int
}
▶ 示例:方向限制实战(管道模式)
package main
import (
"fmt"
"strings"
)
// 阶段 1:只写
func stage1(names []string, out chan<- string) {
for _, name := range names {
out <- strings.ToUpper(name)
}
close(out)
}
// 阶段 2:读写(从 in 读,处理后写到 out)
func stage2(in <-chan string, out chan<- string) {
for name := range in {
out <- "Hello, " + name + "!"
}
close(out)
}
// 阶段 3:只读
func stage3(in <-chan string) {
for msg := range in {
fmt.Println(msg)
}
}
func main() {
ch1 := make(chan string, 3)
ch2 := make(chan string, 3)
names := []string{"Alice", "Bob", "Charlie"}
go stage1(names, ch1)
go stage2(ch1, ch2)
stage3(ch2)
}
输出:
Hello, ALICE!
Hello, BOB!
Hello, CHARLIE!
(2) 方向限制用法
| declare | 权限 | 用途 |
|---|---|---|
ch chan T |
读写 | 变量声明 |
ch chan<- T |
只写 | 生产者函数参数 |
ch <-chan T |
只读 | 消费者函数参数 |
7. select 多路复用入门
(1) select 基础
package main
import (
"fmt"
"time"
)
func main() {
ch1 := make(chan string)
ch2 := make(chan string)
go func() {
time.Sleep(100 * time.Millisecond)
ch1 <- "来自 ch1"
}()
go func() {
time.Sleep(200 * time.Millisecond)
ch2 <- "来自 ch2"
}()
select {
case msg := <-ch1:
fmt.Println(msg)
case msg := <-ch2:
fmt.Println(msg)
case <-time.After(300 * time.Millisecond):
fmt.Println("超时")
}
}
▶ 示例:select 实现超时控制
package main
import (
"fmt"
"time"
)
func longOperation(result chan<- string) {
time.Sleep(3 * time.Second)
result <- "完成"
}
func main() {
result := make(chan string)
go longOperation(result)
select {
case res := <-result:
fmt.Println(res)
case <-time.After(1 * time.Second):
fmt.Println("操作超时!")
}
}
输出:
操作超时!
8. 常见死锁场景
// 死锁 1:无缓冲 channel 只有发送没有接收
func deadlock1() {
ch := make(chan int)
ch <- 42 // fatal error: all goroutines are asleep - deadlock!
}
// 死锁 2:main goroutine 自己等自己
func deadlock2() {
ch := make(chan int)
<-ch // fatal error: deadlock
}
// 死锁 3:多个 goroutine 互相等
func deadlock3() {
ch1 := make(chan int)
ch2 := make(chan int)
go func() {
<-ch1 // 等 ch1
ch2 <- 1
}()
<-ch2 // 等 ch2
ch1 <- 1 // 永远到不了这行
}
(2) 死锁 4 种场景速查
| 场景 | 原因 | 解决 |
|---|---|---|
| 只有发送无接收 | 无缓冲 channel 阻塞 | 确保有消费者 |
| 只有接收无发送 | channel 为空且无发送方 | 确保有生产者 |
| goroutine 互相等待 | A 等 B 的 ch,B 等 A 的 ch | 重新设计依赖顺序 |
| nil channel 操作 | 向/从 nil channel 永久阻塞 | 初始化 channel |
9. 完整示例:生产者-消费者管道
// pipeline.go
package main
import (
"fmt"
"math/rand"
"sync"
"time"
)
// 工作单元
type Job struct {
ID int
Payload string
}
type Result struct {
Job Job
Output string
Err error
Duration time.Duration
}
// 生产者:生成任务(只写 channel)
func producer(jobs chan<- Job, count int) {
for i := 1; i <= count; i++ {
jobs <- Job{
ID: i,
Payload: fmt.Sprintf("task-%d", i),
}
time.Sleep(time.Duration(rand.Intn(50)) * time.Millisecond)
}
close(jobs)
}
// 消费者:处理任务(读 jobs,写 results)
func consumer(id int, jobs <-chan Job, results chan<- Result, wg *sync.WaitGroup) {
defer wg.Done()
for job := range jobs {
start := time.Now()
// 模拟处理
time.Sleep(time.Duration(50+rand.Intn(100)) * time.Millisecond)
output := fmt.Sprintf("consumer-%d processed %s", id, job.Payload)
results <- Result{
Job: job,
Output: output,
Duration: time.Since(start),
}
}
}
// 收集器:收集并打印结果(只读 channel)
func collector(results <-chan Result, done chan<- struct{}) {
var total time.Duration
count := 0
for r := range results {
count++
total += r.Duration
status := "OK"
if r.Err != nil {
status = "ERR"
}
fmt.Printf("[%s] Job#%d: %s (%v)\n",
status, r.Job.ID, r.Output, r.Duration)
}
if count > 0 {
fmt.Printf("\n共 %d 个任务, 平均 %.2f ms\n",
count, float64(total.Milliseconds())/float64(count))
}
close(done)
}
func main() {
const (
jobCount = 20
workerCount = 3
)
jobs := make(chan Job, 10)
results := make(chan Result, 10)
done := make(chan struct{})
// 1 个生产者
go producer(jobs, jobCount)
// N 个消费者
var wg sync.WaitGroup
for w := 1; w <= workerCount; w++ {
wg.Add(1)
go consumer(w, jobs, results, &wg)
}
// 等待所有消费者完成,然后关闭 results
go func() {
wg.Wait()
close(results)
}()
// 收集器(main goroutine)
collector(results, done)
<-done
}
预期输出:
[OK] Job#1: consumer-2 processed task-1 (85ms)
[OK] Job#2: consumer-1 processed task-2 (92ms)
[OK] Job#3: consumer-3 processed task-3 (78ms)
...
[OK] Job#20: consumer-1 processed task-20 (112ms)
共 20 个任务, 平均 87.45 ms
sequenceDiagram
participant P as Producer
participant C as Channel
participant W as Consumer
Note over P: 发送方负责 close
P->>C: ch <- job (发送)
activate C
Note over C: 无缓冲:同步等待<br/>有缓冲:入队
C->>W: job := <-ch (接收)
deactivate C
Note over W: 处理任务
W-->>P: 继续循环
Note over P: close(ch)
Note over C: 剩余数据可读
Note over W: for range 自动退出
❓ 常见问题
v, ok := <-ch 的 ok=false 表示已关闭且无数据。runtime/chan.go 中 hchan 结构体。sem := make(chan struct{}, 10),操作前 sem <- struct{}{},完成后 <-sem。缓冲满时阻塞——自然限流。📖 小节
- channel 创建:
make(chan T)无缓冲,make(chan T, n)有缓冲 - 发送
ch <- v可能阻塞,接收v := <-ch可能阻塞 - 无缓冲 = 同步握手,有缓冲 = 异步队列
close(ch)由发送方调用,接收方用for range遍历v, ok := <-ch检查 channel 是否关闭- 方向限制:
chan<-只写,<-chan只读(编译期检查) - select 多路复用监听多个 channel
- 常见死锁:无对方/循环依赖/nil channel
📝 作业
-
基础题(难度⭐):写一个程序:启动一个 goroutine 发送 0~9 十个数字到无缓冲 channel,main goroutine 接收并打印。观察发送和接收的交替顺序。
-
进阶题(难度⭐⭐):实现一个扇出(fan-out)模式:一个生产者发送 100 个任务到 channel,启动 5 个消费者 goroutine 从同一个 channel 读取处理,每个消费者打印自己的 ID 和任务编号。要求用有缓冲 channel + close + for range。
-
挑战题(难度⭐⭐⭐):实现一个三阶段管道(pipeline):阶段 1 生成随机数字(
[]int);阶段 2 过滤出偶数;阶段 3 计算平方和。每个阶段是一个独立的 goroutine,用 channel 连接。要求:(1) 各阶段用方向限制;(2) 支持动态调整阶段数;(3) 用 select 实现优雅退出。