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

243 lines
6.7 KiB
Markdown
Raw Normal View History

2026-06-07 11:08:10 +08:00
---
2026-06-07 12:14:39 +08:00
tags: [go, golang, Channel, 并发, CSP]
create time: 2026-06-07 14:40
2026-06-07 11:08:10 +08:00
---
# Channel
2026-06-07 12:14:39 +08:00
## 概述
Channel 是 Go 中 goroutine 之间通信的核心机制。Go 倡导"以通信共享内存,而非共享内存来通信",channel 正是这一哲学(CSP 模型)的具象化体现。本文涵盖 channel 的创建、操作、单向/双向类型、缓冲语义以及实战技巧。
## 正文
### 什么是 Channel?
> [!question] 💭 思考
> 如果多个 goroutine 需要交换数据,除了用锁保护共享变量,还有没有更安全的方式?
官方定义:Channel 是一个类型化的管道,通过它你可以发送和接收值。
2026-06-07 11:08:10 +08:00
```go
2026-06-07 12:14:39 +08:00
ch := make(chan int) // 创建一个传递 int 的 channel
ch <- 42 // 发送:向 ch 写入 42
v := <-ch // 接收:从 ch 读取值到 v
2026-06-07 11:08:10 +08:00
```
2026-06-07 12:14:39 +08:00
> [!info] ℹ️ 核心理念
> **"Don't communicate by sharing memory; share memory by communicating."** — Rob Pike
> 与其用锁保护共享数据,不如让数据在 goroutine 之间流动——拥有数据的 goroutine 就是唯一能修改它的那个。
### 创建与初始化
2026-06-07 11:08:10 +08:00
```go
2026-06-07 12:14:39 +08:00
// 无缓冲 channel(同步模式)
ch := make(chan int)
// 有缓冲 channel(异步模式,容量为 3)
ch := make(chan int, 3)
2026-06-07 11:08:10 +08:00
```
2026-06-07 12:14:39 +08:00
> [!warning] ⚠️ 常见错误
> - **未初始化的 channel 是 `nil`**,对 nil channel 发送/接收会永久阻塞
> - **重复关闭 channel 会 panic**
> - **向已关闭的 channel 发送会 panic**
### 基本操作
| 操作 | 语法 | 说明 |
|------|------|------|
| 发送 | `ch <- value` | 向 channel 写入,可能阻塞 |
| 接收 | `<-ch` | 从 channel 读取,可能阻塞 |
| 带值接收 | `v := <-ch` | 读取值并赋值 |
| 关闭 | `close(ch)` | 标记 channel 不再发送数据 |
2026-06-07 11:08:10 +08:00
```go
package main
2026-06-07 12:14:39 +08:00
import "fmt"
2026-06-07 11:08:10 +08:00
func main() {
2026-06-07 12:14:39 +08:00
ch := make(chan int)
2026-06-07 11:08:10 +08:00
go func() {
2026-06-07 12:14:39 +08:00
ch <- 42 // 生产者:发送数据
close(ch) // 生产完毕,关闭 channel
2026-06-07 11:08:10 +08:00
}()
2026-06-07 12:14:39 +08:00
v := <-ch // 消费者:接收数据
fmt.Println(v) // 42
2026-06-07 11:08:10 +08:00
}
```
2026-06-07 12:14:39 +08:00
### 关闭 Channel 后的行为
2026-06-07 11:08:10 +08:00
2026-06-07 12:14:39 +08:00
> [!question] 💭 思考
> 关闭一个 channel 后还能读吗?还能写吗?读完已有数据后再读会怎样?
2026-06-07 11:08:10 +08:00
2026-06-07 12:14:39 +08:00
| 操作 | 未关闭 | 已关闭(有数据) | 已关闭(空) |
|------|--------|-----------------|-------------|
| 发送 | ✅ 正常 | ❌ **panic** | ❌ **panic** |
| 接收 | ✅ 正常 | ✅ 返回已有数据 | ✅ 返回零值 |
| 关闭 | ✅ 正常 | ✅ 可重复关闭检查 | ❌ panic(重复关闭) |
2026-06-07 11:08:10 +08:00
```go
2026-06-07 12:14:39 +08:00
ch := make(chan int, 5)
ch <- 1
close(ch)
// 关闭后仍可读取
for i := 0; i < 5; i++ {
v := <-ch
fmt.Println(v) // 第1次: 1, 之后全是 0
}
2026-06-07 11:08:10 +08:00
```
2026-06-07 12:14:39 +08:00
#### 安全读取:ok 模式
2026-06-07 11:08:10 +08:00
2026-06-07 12:14:39 +08:00
当需要从已关闭的 channel 区分"有数据"和"已关闭"时,使用多返回值形式:
2026-06-07 11:08:10 +08:00
2026-06-07 12:14:39 +08:00
```go
v, ok := <-ch
if ok {
fmt.Println("收到数据:", v)
} else {
fmt.Println("channel 已关闭,无更多数据")
2026-06-07 11:08:10 +08:00
}
```
2026-06-07 12:14:39 +08:00
#### for-range 读取
2026-06-07 11:08:10 +08:00
2026-06-07 12:14:39 +08:00
这是最优雅的 channel 消费方式——自动在 channel 关闭时退出循环:
2026-06-07 11:08:10 +08:00
2026-06-07 12:14:39 +08:00
```go
go func() {
for v := range ch { // channel 关闭后自动退出
fmt.Println("收到:", v)
}
fmt.Println("channel 已关闭,循环结束")
}()
2026-06-07 11:08:10 +08:00
2026-06-07 12:14:39 +08:00
close(ch) // 触发 for-range 退出
2026-06-07 11:08:10 +08:00
```
2026-06-07 12:14:39 +08:00
> [!tip] 💡 最佳实践
> **发送端负责关闭,接收端不负责关闭**。如果一个 channel 有多个发送方,谁该关闭它?这种模糊场景应尽量避免。
### 有缓冲 vs 无缓冲
```mermaid
flowchart LR
subgraph Unbuf["无缓冲 channel"]
S[发送方] -->|"阻塞直到接收方就绪"| R[接收方]
end
subgraph Buf["有缓冲 channel (cap=3)"]
B["缓冲区 📦📦"] -.->|剩余容量| S2[发送方]
R2[接收方] -.->|有空位| S2
R2 -->|"取出"| B
end
2026-06-07 11:08:10 +08:00
```
2026-06-07 12:14:39 +08:00
- **无缓冲 channel**:发送和接收同步发生——发送方阻塞直到接收方准备好,反之亦然。适用于需要严格配对的生产者-消费者场景。
- **有缓冲 channel**:发送方在缓冲区未满时无需等待即可返回;接收方在缓冲区非空时无需等待即可拿到数据。适用于解耦生产速率和消费速率的场景。
2026-06-07 11:08:10 +08:00
2026-06-07 12:14:39 +08:00
> [!warning] ⚠️ 注意
> 缓冲区满了以后,有缓冲 channel 也会退化为同步模式——发送方将被阻塞。长期满队列意味着消费者跟不上生产者的节奏,应考虑增加消费者数量或扩大缓冲区。
2026-06-07 11:08:10 +08:00
2026-06-07 12:14:39 +08:00
### 单向 Channel
2026-06-07 11:08:10 +08:00
2026-06-07 12:14:39 +08:00
有时我们希望限制 channel 的使用方向,比如在函数签名中明确表达"这个参数只用于发送"或"只用于接收":
2026-06-07 11:08:10 +08:00
```go
2026-06-07 12:14:39 +08:00
// 只发送 channel —— 只能 ch<-value
sendCh := make(chan int)
var sendOnly chan<- int = sendCh
2026-06-07 11:08:10 +08:00
2026-06-07 12:14:39 +08:00
// 只接收 channel —— 只能 <-ch
var recvOnly <-chan int = sendCh
```
2026-06-07 11:08:10 +08:00
2026-06-07 12:14:39 +08:00
```go
func producer(out chan<- int) {
for i := 0; i < 5; i++ {
out <- i // 只能发,不能收
}
close(out)
2026-06-07 11:08:10 +08:00
}
2026-06-07 12:14:39 +08:00
func consumer(in <-chan int) {
for v := range in { // 只能收,不能发
fmt.Println("received:", v)
}
}
```
> [!note] 📝 关键规则
> 只有**接收方**才能关闭 channel。单向 channel 的类型转换是单向的:双向可以转为单向,单向不能转回双向。
### 经典模式:扇入扇出
2026-06-07 11:08:10 +08:00
2026-06-07 12:14:39 +08:00
> [!question] 💭 思考
> 如果有 10 个任务要并行处理,但结果需要汇总到一个地方,怎么设计?
2026-06-07 11:08:10 +08:00
2026-06-07 12:14:39 +08:00
```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)
}
2026-06-07 11:08:10 +08:00
}
```
2026-06-07 12:14:39 +08:00
### Channel 实现互斥锁
> [!question] 💭 思考
> channel 本身已经是并发安全的了,能不能利用这一点来代替 mutex?
一个容量为 1 的 channel 可以充当信号量:
```go
ch := make(chan struct{}, 1)
ch <- struct{}{} // 获取"锁"
// 临界区:同时只有一个 goroutine 能执行
*counter++
<-ch // 释放"锁"
```
2026-06-07 11:08:10 +08:00
2026-06-07 12:14:39 +08:00
> [!tip] 💡 何时用 Channel vs Mutex?
> - 优先用 **channel**:goroutine 间的数据传递、事件通知、生命周期管理
> - 优先用 **mutex**:保护共享变量的并发访问
> - 两者不互斥,复杂场景中经常配合使用
2026-06-07 11:08:10 +08:00
2026-06-07 12:14:39 +08:00
## 关联笔记
2026-06-07 11:08:10 +08:00
2026-06-07 12:14:39 +08:00
- [[hzh/GolangStar/Go语言进阶/Select]]
- [[hzh/GolangStar/Go语言进阶/Sync]]
- [[hzh/GolangStar/Go语言进阶/Goroutine]]
- [[hzh/GolangStar/Go语言原理/channel原理]]