This repository has been archived on 2026-05-19. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
obsidian/DEV/GO/Channel详解.md
T

334 lines
8.5 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, 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/内存分配与逃逸分析]]