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