334 lines
8.5 KiB
Markdown
334 lines
8.5 KiB
Markdown
|
|
---
|
|||
|
|
tags: [go, golang, runtime, channel, 并发, sync]
|
|||
|
|
create time: 2026-04-24 10:00
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# Channel 详解
|
|||
|
|
|
|||
|
|
## 概述
|
|||
|
|
|
|||
|
|
Channel 是 Go 中最核心的并发原语之一,遵循 **"不要通过共享内存来通信,而是通过通信来共享内存"** 的设计哲学。它是 goroutine 之间传递数据的同步机制,也是 Go 并发编程的灵魂。
|
|||
|
|
|
|||
|
|
> **思考**:既然有 Mutex 可以做同步,为什么还要发明 Channel?Channel 相比 Mutex 的抽象层次有什么优势?
|
|||
|
|
> 详见 [[DEV/GO/Channel vs Mutex]]。
|
|||
|
|
|
|||
|
|
Channel 的本质是一个**线程安全的 FIFO 队列**,支持三种操作:**发送**、**接收**、**关闭**。
|
|||
|
|
|
|||
|
|
## Channel 的基础类型
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
// 声明三种 channel
|
|||
|
|
var ch1 chan int // nil channel(未初始化)
|
|||
|
|
var ch2 chan int = make(chan int) // 无缓冲 channel
|
|||
|
|
var ch3 chan int = make(chan int, 10) // 有缓冲 channel,容量 10
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 无缓冲 Channel
|
|||
|
|
|
|||
|
|
无缓冲 channel 的 `len() == 0`、`cap() == 0`。
|
|||
|
|
|
|||
|
|
**核心语义:发送和接收必须同时完成,是同步的。**
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
func main() {
|
|||
|
|
ch := make(chan int) // 无缓冲
|
|||
|
|
|
|||
|
|
go func() {
|
|||
|
|
ch <- 42 // 发送:如果没人接收,会阻塞
|
|||
|
|
fmt.Println("sent 42")
|
|||
|
|
}()
|
|||
|
|
|
|||
|
|
val := <-ch // 接收:如果没人发送,会阻塞
|
|||
|
|
fmt.Println("received:", val) // 输出: received: 42
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**无缓冲 channel 的行为图解**:
|
|||
|
|
|
|||
|
|
```mermaid
|
|||
|
|
sequenceDiagram
|
|||
|
|
participant Sender as 发送方 goroutine
|
|||
|
|
participant Channel as 无缓冲 channel
|
|||
|
|
participant Receiver as 接收方 goroutine
|
|||
|
|
|
|||
|
|
Sender->>Channel: ch <- 42
|
|||
|
|
Note over Sender,Channel: 双方同时就绪
|
|||
|
|
Channel->>Receiver: 传递 42
|
|||
|
|
Receiver->>Channel: <-ch 接收完成
|
|||
|
|
Note over Receiver,Channel: 发送和接收同时完成
|
|||
|
|
Sender->>Sender: 继续执行
|
|||
|
|
Receiver->>Receiver: 继续执行
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
> **关键点**:无缓冲 channel 实现了 goroutine 之间的**同步屏障**。发送方阻塞到接收方 ready,接收方阻塞到发送方 ready。
|
|||
|
|
|
|||
|
|
## 有缓冲 Channel
|
|||
|
|
|
|||
|
|
有缓冲 channel 的 `len()` 是当前元素数量,`cap()` 是缓冲区容量。
|
|||
|
|
|
|||
|
|
**核心语义:缓冲区未满时发送不阻塞,缓冲区非空时接收不阻塞。**
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
func main() {
|
|||
|
|
ch := make(chan int, 3) // 缓冲容量 3
|
|||
|
|
|
|||
|
|
ch <- 1 // len=0→1, 不阻塞(缓冲区有空位)
|
|||
|
|
ch <- 2 // len=1→2, 不阻塞
|
|||
|
|
ch <- 3 // len=2→3, 不阻塞(缓冲区满)
|
|||
|
|
|
|||
|
|
fmt.Println(len(ch)) // 输出: 3
|
|||
|
|
fmt.Println(cap(ch)) // 输出: 3
|
|||
|
|
|
|||
|
|
// 缓冲区已满,下一条发送会阻塞
|
|||
|
|
// ch <- 4 // ❌ 阻塞!
|
|||
|
|
|
|||
|
|
val := <-ch // len=3→2, 不阻塞
|
|||
|
|
fmt.Println("received:", val) // 输出: received: 1
|
|||
|
|
fmt.Println(len(ch)) // 输出: 2
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**缓冲区的内部结构**:
|
|||
|
|
|
|||
|
|
```mermaid
|
|||
|
|
graph LR
|
|||
|
|
subgraph Buffer["channel 缓冲区 (cap=4)"]
|
|||
|
|
B1[元素 1]
|
|||
|
|
B2[元素 2]
|
|||
|
|
B3[元素 3]
|
|||
|
|
B4[空位]
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
subgraph Send["发送方"]
|
|||
|
|
S["ch <- 4"]
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
subgraph Recv["接收方"]
|
|||
|
|
R["<- ch"]
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
S -->|"缓冲区未满,直接写入"| Buffer
|
|||
|
|
Buffer -->|"缓冲区非空,直接读取"| R
|
|||
|
|
|
|||
|
|
classDef buf fill:#e3f2fd,stroke:#1565c0
|
|||
|
|
classDef send fill:#fff3e0,stroke:#e65100
|
|||
|
|
classDef recv fill:#e8f5e9,stroke:#2e7d32
|
|||
|
|
class Buffer buf
|
|||
|
|
class S send
|
|||
|
|
class R recv
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### len() vs cap()
|
|||
|
|
|
|||
|
|
| 属性 | 含义 | 示例 |
|
|||
|
|
|------|------|------|
|
|||
|
|
| `len(ch)` | 当前缓冲区中的元素数量 | `0 ~ cap` 之间变化 |
|
|||
|
|
| `cap(ch)` | 缓冲区的最大容量 | 创建时确定,不可改变 |
|
|||
|
|
|
|||
|
|
> **思考**:`len()` 和 `cap()` 分别在什么时机更新?如果一个 goroutine 在发送,另一个在接收,`len()` 会正确反映当前值吗?
|
|||
|
|
|
|||
|
|
## Channel 的关闭
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
ch := make(chan int, 3)
|
|||
|
|
|
|||
|
|
ch <- 1
|
|||
|
|
ch <- 2
|
|||
|
|
|
|||
|
|
close(ch) // 关闭 channel
|
|||
|
|
|
|||
|
|
// 关闭后还能接收已发送的数据
|
|||
|
|
val, ok := <-ch // val=1, ok=true
|
|||
|
|
val, ok := <-ch // val=2, ok=true
|
|||
|
|
val, ok := <-ch // val=0 (零值), ok=false // 缓冲区空了,收到零值
|
|||
|
|
|
|||
|
|
// 从关闭的 channel 接收,ok 始终为 false
|
|||
|
|
val, ok := <-ch // val=0, ok=false
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**close() 的规则**:
|
|||
|
|
|
|||
|
|
| 操作 | 合法? | 后果 |
|
|||
|
|
|------|--------|------|
|
|||
|
|
| `close(ch)` — 正常关闭 | ✅ | 接收方可以读完残留数据,后续接收返回零值和 `false` |
|
|||
|
|
| 向已关闭的 channel 发送 | ❌ | panic: send on closed channel |
|
|||
|
|
| 关闭 nil channel | ❌ | panic: close of nil channel |
|
|||
|
|
| 关闭已关闭的 channel | ❌ | panic: close of closed channel |
|
|||
|
|
|
|||
|
|
> **核心原则**:**发送方负责关闭 channel,接收方不负责关闭。** 多个发送方场景下,建议用 `sync.Once` 确保只关闭一次。
|
|||
|
|
|
|||
|
|
## nil Channel
|
|||
|
|
|
|||
|
|
nil channel 是一个未初始化的 channel(值为 `nil`)。
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
var ch chan int // nil channel
|
|||
|
|
|
|||
|
|
// 向 nil channel 发送 → 永远阻塞
|
|||
|
|
// ch <- 1 // ❌ 死锁
|
|||
|
|
|
|||
|
|
// 从 nil channel 接收 → 永远阻塞
|
|||
|
|
// <-ch // ❌ 死锁
|
|||
|
|
|
|||
|
|
// 关闭 nil channel → panic
|
|||
|
|
// close(ch) // ❌ panic: close of nil channel
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**nil channel 的实际用途**:用于 `select` 中禁用某个分支。
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
func main() {
|
|||
|
|
ch1 := make(chan int)
|
|||
|
|
var ch2 chan int // nil
|
|||
|
|
|
|||
|
|
select {
|
|||
|
|
case v := <-ch1:
|
|||
|
|
fmt.Println("received from ch1:", v)
|
|||
|
|
case <-ch2:
|
|||
|
|
fmt.Println("from ch2 (never reaches here)")
|
|||
|
|
}
|
|||
|
|
// select 永远阻塞在 ch1 上,ch2 分支被静默禁用
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
> **思考**:为什么 nil channel 是"永远阻塞"而不是"立即返回"?这体现了 Go 对 nil 的哪种设计哲学?
|
|||
|
|
|
|||
|
|
## Channel 死锁场景
|
|||
|
|
|
|||
|
|
### 场景 1:向无人读的 channel 写
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
func main() {
|
|||
|
|
ch := make(chan int)
|
|||
|
|
ch <- 42 // 阻塞:没有接收方
|
|||
|
|
// 输出: all goroutines are asleep - deadlock!
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 场景 2:读无人写的 channel
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
func main() {
|
|||
|
|
ch := make(chan int)
|
|||
|
|
<-ch // 阻塞:没有发送方
|
|||
|
|
// 输出: all goroutines are asleep - deadlock!
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 场景 3:读写 nil channel
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
func main() {
|
|||
|
|
var ch chan int // nil
|
|||
|
|
<-ch // 永远阻塞 → 死锁
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 场景 4:goroutine 中读写已关闭的 channel
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
func main() {
|
|||
|
|
ch := make(chan int)
|
|||
|
|
close(ch)
|
|||
|
|
|
|||
|
|
go func() {
|
|||
|
|
ch <- 1 // panic: send on closed channel
|
|||
|
|
}()
|
|||
|
|
|
|||
|
|
<-ch // 正常接收
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## for-range 与 Channel
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
func main() {
|
|||
|
|
ch := make(chan int, 3)
|
|||
|
|
ch <- 1
|
|||
|
|
ch <- 2
|
|||
|
|
close(ch) // 必须关闭,for-range 才知道何时结束
|
|||
|
|
|
|||
|
|
for v := range ch { // 自动读取直到 channel 关闭且空
|
|||
|
|
fmt.Println(v) // 输出: 1, 2
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
> **思考**:如果不对 channel 调用 `close()`,`for range` 会发生什么?这与无缓冲 channel 的死锁有什么区别?
|
|||
|
|
|
|||
|
|
## 常见模式
|
|||
|
|
|
|||
|
|
### Worker Pool(工作池)
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
func worker(id int, jobs <-chan int, results chan<- int) {
|
|||
|
|
for j := range jobs {
|
|||
|
|
results <- j * 2
|
|||
|
|
}
|
|||
|
|
fmt.Printf("worker %d done\n", id)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
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 <= 5; j++ {
|
|||
|
|
jobs <- j
|
|||
|
|
}
|
|||
|
|
close(jobs) // 发送完毕后关闭
|
|||
|
|
|
|||
|
|
// 等待结果
|
|||
|
|
for i := 1; i <= 5; i++ {
|
|||
|
|
<-results
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 超时控制
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
select {
|
|||
|
|
case v := <-ch:
|
|||
|
|
fmt.Println("received:", v)
|
|||
|
|
case <-time.After(5 * time.Second):
|
|||
|
|
fmt.Println("timed out after 5s")
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## Channel 内部实现要点
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
// Go 源码简化版 channel 结构
|
|||
|
|
type hchan struct {
|
|||
|
|
buf unsafe.Pointer // 缓冲区(有缓冲时)
|
|||
|
|
elemsize uint16 // 每个元素的大小
|
|||
|
|
closed uint32 // 是否关闭
|
|||
|
|
elemtype *type // 元素类型
|
|||
|
|
sendx uint // 发送索引
|
|||
|
|
recvx uint // 接收索引
|
|||
|
|
recvq waitq // 等待接收的 goroutine 队列
|
|||
|
|
sendq waitq // 等待发送的 goroutine 队列
|
|||
|
|
|
|||
|
|
lock mutex // 保护所有字段的互斥锁
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- Channel 是**并发安全**的,内部使用 `mutex` 保护所有操作
|
|||
|
|
- 无缓冲 channel:直接 sender ↔ receiver 握手(锁粒度极小)
|
|||
|
|
- 有缓冲 channel:先写入缓冲区(`buf` 循环缓冲区),满时才阻塞
|
|||
|
|
- `recvq` 和 `sendq` 是 **Goroutine 等待队列**(与 GMP 模型联动)
|
|||
|
|
|
|||
|
|
> **关键理解**:Channel 的锁只保护缓冲区操作本身,不影响 goroutine 的调度——GMP 模型负责调度等待队列中的 goroutine。
|
|||
|
|
|
|||
|
|
## 关联笔记
|
|||
|
|
|
|||
|
|
- [[DEV/GO/GMP调度模型]]
|
|||
|
|
- [[DEV/GO/Goroutine泄漏排查]]
|
|||
|
|
- [[DEV/GO/内存分配与逃逸分析]]
|