Go Channel:通信实现共享内存
Channel 是 Go 语言并发编程的基石之一。它不仅是 goroutine 之间通信的管道,更承载了同步与数据传递的双重职责。
本文将系统梳理 Channel 的创建、读写、关闭的各种用法与注意事项,助你写出既优雅又健壮的并发代码。
1. 构造器函数:创建 Channel
Go 使用内置的 make 函数创建 Channel,可指定是否带缓冲区。
chUnbuf := make(chan int)
chBuf := make(chan int, 3)
|
| 类型 |
特点 |
| 无缓冲 |
同步通信:发送方阻塞直到接收方准备好 |
| 有缓冲 |
异步通信:缓冲区满前不阻塞发送,空时接收阻塞 |
最佳实践:优先使用无缓冲通道,除非你有充分的理由需要缓冲区(如平滑流量峰值)。
2. 从 Channel 读取数据
读操作有丰富的变体,适应不同场景。
2.1 正常读取(标准写法)
data := <-ch data, ok := <-ch
|
2.2 忽略读取(扔掉数据)
常用于信号量或事件通知:
done := make(chan struct{}) go func() { doWork() done <- struct{}{} }() <-done
|
2.3 判断读(检测通道关闭)
value, ok := <-ch if !ok { fmt.Println("通道已关闭,value 为零值") } else { fmt.Println("读到:", value) }
|
2.4 阻塞读的三种情况
| 场景 |
行为 |
| 无缓冲通道,无发送方就绪 |
当前 goroutine 永久阻塞,直到有发送者 |
| 有缓冲通道,缓冲区为空 |
当前 goroutine 永久阻塞,直到有数据写入 |
从 nil channel 读取 |
永久阻塞(无法唤醒,属于程序 bug) |
var nilCh chan int <-nilCh
|
2.5 优雅关闭读取:for range
for range 会持续从通道读取值,直到通道被关闭且缓冲区为空。
ch := make(chan int, 5)
for v := range ch { fmt.Println(v) }
for { v, ok := <-ch if !ok { break } fmt.Println(v) }
|
2.6 用 select 实现非阻塞读
select { case v, ok := <-ch: if ok { fmt.Println("读到:", v) } else { fmt.Println("通道已关闭") } default: fmt.Println("通道暂时无数据,不阻塞") }
|
3. 向 Channel 写入数据
写操作相对单纯,但某些错误会直接导致 panic。
3.1 正常写入
3.2 阻塞写的三种情况
| 场景 |
行为 |
| 无缓冲通道,无接收方就绪 |
当前 goroutine 阻塞,直到有接收者 |
| 有缓冲通道,缓冲区已满 |
当前 goroutine 阻塞,直到有接收者腾出空间 |
向 nil channel 写入 |
永久阻塞 |
var nilCh chan int nilCh <- 1
|
3.3 写已关闭的通道 → panic
ch := make(chan int) close(ch) ch <- 1
|
这是 Go 运行时最有名的“礼节”之一:发送者负责关闭通道,接收者从不关闭。
4. 关闭 Channel
关闭通道是一种广播行为,常用于通知接收者“没有更多数据了”。
4.1 正确关闭语法
4.2 关闭的三大原则
- 由发送方关闭,绝不是接收方。
- 关闭已经关闭的通道 → panic
- 关闭
nil 通道 → panic
ch := make(chan int) close(ch) close(ch)
var nilCh chan int close(nilCh)
|
4.3 关闭后接收方的行为
- 通道关闭后,已缓冲的数据仍然可以被正常读取(按 FIFO 顺序)。
- 缓冲区清空后,再读取会立即返回零值 + ok=false。
ch := make(chan int, 2) ch <- 1 ch <- 2 close(ch) fmt.Println(<-ch) fmt.Println(<-ch) fmt.Println(<-ch)
|
4.4 安全关闭模式:使用 sync.Once
为避免多次关闭导致 panic,可以用 sync.Once:
var once sync.Once once.Do(func() { close(ch) })
|
5. 综合示例:生产者-消费者模型
func producer(ch chan<- int, wg *sync.WaitGroup) { defer wg.Done() for i := 0; i < 5; i++ { ch <- i fmt.Printf("produced: %d\n", i) } close(ch) }
func consumer(ch <-chan int, wg *sync.WaitGroup) { defer wg.Done() for v := range ch { fmt.Printf("consumed: %d\n", v) } }
func main() { ch := make(chan int, 2) var wg sync.WaitGroup wg.Add(2) go producer(ch, &wg) go consumer(ch, &wg) wg.Wait() }
|
6. 常见陷阱与总结
| 操作 |
未关闭通道 |
已关闭通道 |
nil 通道 |
读 (<-ch) |
阻塞或正常读取 |
返回零值 + ok=false |
永久阻塞 |
写 (ch<-) |
阻塞或正常写入 |
panic |
永久阻塞 |
关 (close) |
正常关闭 |
panic |
panic |
最佳实践清单
- ✅ 使用
for range 消费通道,自动处理关闭。
- ✅ 发送方关闭通道,接收方绝不关闭。
- ✅ 用
ok 模式判断通道是否关闭。
- ✅ 使用
select + default 实现非阻塞 I/O。
- ✅ 用
sync.Once 保护可能重复关闭的通道。
- ❌ 不要向已关闭的 channel 发送数据。
- ❌ 不要关闭一个其他 goroutine 还在写入的 channel(导致 panic)。
- ❌ 不要依赖
len(ch) 来决定读写逻辑(竞争风险)。
掌握 Channel 的用法,你就能写出既清晰又安全的并发 Go 程序。
记住一句 Go 箴言:“Don’t communicate by sharing memory; share memory by communicating.” —— 而这正是 Channel 存在的意义。