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原理]]
|