Files
cs-note/hzh/GolangStar/Go语言进阶/Channel.md
T

243 lines
6.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
tags: [go, golang, Channel, 并发, CSP]
create time: 2026-06-07 14:40
---
# Channel
## 概述
Channel 是 Go 中 goroutine 之间通信的核心机制。Go 倡导"以通信共享内存,而非共享内存来通信",channel 正是这一哲学(CSP 模型)的具象化体现。本文涵盖 channel 的创建、操作、单向/双向类型、缓冲语义以及实战技巧。
## 正文
### 什么是 Channel?
> [!question] 💭 思考
> 如果多个 goroutine 需要交换数据,除了用锁保护共享变量,还有没有更安全的方式?
官方定义:Channel 是一个类型化的管道,通过它你可以发送和接收值。
```go
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 就是唯一能修改它的那个。
### 创建与初始化
```go
// 无缓冲 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 不再发送数据 |
```go
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(重复关闭) |
```go
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 区分"有数据"和"已关闭"时,使用多返回值形式:
```go
v, ok := <-ch
if ok {
fmt.Println("收到数据:", v)
} else {
fmt.Println("channel 已关闭,无更多数据")
}
```
#### for-range 读取
这是最优雅的 channel 消费方式——自动在 channel 关闭时退出循环:
```go
go func() {
for v := range ch { // channel 关闭后自动退出
fmt.Println("收到:", v)
}
fmt.Println("channel 已关闭,循环结束")
}()
close(ch) // 触发 for-range 退出
```
> [!tip] 💡 最佳实践
> **发送端负责关闭,接收端不负责关闭**。如果一个 channel 有多个发送方,谁该关闭它?这种模糊场景应尽量避免。
### 有缓冲 vs 无缓冲
```mermaid
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 的使用方向,比如在函数签名中明确表达"这个参数只用于发送"或"只用于接收":
```go
// 只发送 channel —— 只能 ch<-value
sendCh := make(chan int)
var sendOnly chan<- int = sendCh
// 只接收 channel —— 只能 <-ch
var recvOnly <-chan int = sendCh
```
```go
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 个任务要并行处理,但结果需要汇总到一个地方,怎么设计?
```go
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 可以充当信号量:
```go
ch := make(chan struct{}, 1)
ch <- struct{}{} // 获取"锁"
// 临界区:同时只有一个 goroutine 能执行
*counter++
<-ch // 释放"锁"
```
> [!tip] 💡 何时用 Channel vs Mutex?
> - 优先用 **channel**:goroutine 间的数据传递、事件通知、生命周期管理
> - 优先用 **mutex**:保护共享变量的并发访问
> - 两者不互斥,复杂场景中经常配合使用
## 关联笔记
- [[hzh/GolangStar/Go语言进阶/Select]]
- [[hzh/GolangStar/Go语言进阶/Sync]]
- [[hzh/GolangStar/Go语言进阶/Goroutine]]
- [[hzh/GolangStar/Go语言原理/channel原理]]