channel 真的是"通信而非共享"吗?本文读了源码才明白 Go 并发设计的精髓
前言
"老王,为什么本文的 channel 总是死锁?" 新来的实习生小张一脸困惑地问本文。
本文看了看他的代码,发现他在无缓冲 channel 里发送了数据,但没有对应的接收。"你这是典型的同步通信没配对啊!"
"什么是同步通信?channel 不就是传数据的吗?"
看来得从 channel 的本质讲起了。今天本文们就从源码层面深入剖析 channel 的设计意图和边界。
一、 底层原理
1.1 channel 的本质:带锁的队列
channel 的底层实现是一个带锁的环形缓冲区:
graph TD A["发送方 G"] --> B["channel"] C["接收方 G"] --> D["channel"] B --> E["环形缓冲区"] B --> F["发送等待队列"] D --> G["接收等待队列"] E --> H["加锁"] H --> I["入队"] I --> J["唤醒接收方"] J --> K["接收方执行"]核心结构:
hchan:channel 头部结构体buf:环形缓冲区sendq:发送等待队列recvq:接收等待队列lock:互斥锁
1.2 channel 的设计哲学
| 设计原则 | 解释 | 代码体现 |
|---|---|---|
| 通信而非共享 | 数据通过 channel 传递 | 发送/接收操作 |
| 阻塞等待 | 无数据/缓冲区满时阻塞 | sendq/recvq |
| 原子操作 | 发送/接收是原子的 | 加锁保护 |
| 优雅关闭 | 通知接收方结束 | close 操作 |
二、 快速上手
2.1 channel 的基本使用
package main import ( "fmt" "sync" ) func main() { ch := make(chan int, 3) var wg sync.WaitGroup // 生产者 wg.Add(1) go func() { defer wg.Done() for i := 1; i <= 5; i++ { ch <- i } close(ch) }() // 消费者 wg.Add(1) go func() { defer wg.Done() for val := range ch { fmt.Printf("收到: %d\n", val) } }() wg.Wait() }2.2 无缓冲 vs 有缓冲
// 无缓冲:同步通信 ch := make(chan int) go func() { ch <- 1 }() val := <-ch // 必须配对 // 有缓冲:异步 ch := make(chan int, 10) ch <- 1 // 不会阻塞 ch <- 2 // 不会阻塞三、 核心 API / 深水区
3.1 channel 操作速查
| 操作 | 语法 | 行为 |
|---|---|---|
| 创建 | ch := make(chan T, n) | 无缓冲或缓冲 |
| 发送 | ch <- val | 可能阻塞 |
| 接收 | val := <-ch | 可能阻塞 |
| 关闭 | close(ch) | 通知接收方 |
| 遍历 | for v := range ch | 自动迭代 |
| 超时 | select + time.After | 防止死锁 |
3.2 select 多路复用
select { case v := <-ch1: fmt.Println(v) case v := <-ch2: fmt.Println(v) case <-time.After(time.Second): fmt.Println("超时") default: fmt.Println("没有数据") }3.3 channel 的三种模式
// 1. 单向 channel var sendOnly chan<- int // 只发 var recvOnly <-chan int // 只收 // 2. 管道模式 func generator() <-chan int { out := make(chan int) go func() { for i := 0; i < 10; i++ { out <- i } close(out) }() return out } // 3. fan-in / fan-out func fanIn(chs ...<-chan int) <-chan int { out := make(chan int) var wg sync.WaitGroup for _, ch := range chs { wg.Add(1) go func(c <-chan int) { defer wg.Done() for v := range c { out <- v } }(ch) } go func() { wg.Wait() close(out) }() return out }四、 实战演练
4.1 工作池模式
package main import ( "fmt" "sync" "time" ) type Task struct { ID int Data string } type WorkerPool struct { tasks chan Task results chan string workers int wg sync.WaitGroup } func NewWorkerPool(workers int, queueSize int) *WorkerPool { return &WorkerPool{ tasks: make(chan Task, queueSize), results: make(chan string, queueSize), workers: workers, } } func (wp *WorkerPool) Start() { for i := 0; i < wp.workers; i++ { wp.wg.Add(1) go wp.worker(i) } } func (wp *WorkerPool) worker(id int) { defer wp.wg.Done() for task := range wp.tasks { time.Sleep(10 * time.Millisecond) wp.results <- fmt.Sprintf("工人%d 完成 任务%d", id, task.ID) } } func (wp *WorkerPool) Submit(task Task) { wp.tasks <- task } func (wp *WorkerPool) Stop() { close(wp.tasks) wp.wg.Wait() close(wp.results) } func main() { pool := NewWorkerPool(5, 100) pool.Start() for i := 0; i < 100; i++ { pool.Submit(Task{ID: i, Data: fmt.Sprintf("数据%d", i)}) } pool.Stop() for result := range pool.results { fmt.Println(result) } }五、 避坑指南与最佳实践
💡技巧:生产者 close,消费者 range
这样消费者能感知到结束,不会阻塞。
⚠️警告:不要在循环里 close
close 只能一次,多次 close 会 panic。
✅推荐:用 select 加超时
防止 channel 操作导致永久阻塞。
六、 综合实战演示
6.1 完整的管道模式
package main import ( "fmt" "sync" "time" ) func generator(nums ...int) <-chan int { out := make(chan int) go func() { for _, n := range nums { out <- n } close(out) }() return out } func square(in <-chan int) <-chan int { out := make(chan int) go func() { for n := range in { out <- n * n } close(out) }() return out } func filter(in <-chan int, fn func(int) bool) <-chan int { out := make(chan int) go func() { for n := range in { if fn(n) { out <- n } } close(out) }() return out } func main() { ch := generator(1, 2, 3, 4, 5, 6, 7, 8, 9, 10) squared := square(ch) filtered := filter(squared, func(n int) bool { return n > 20 }) for result := range filtered { fmt.Printf("结果: %d\n", result) } }总结
channel 的精髓:
- 通信而非共享:数据通过 channel 传递,避免共享内存的锁竞争
- 阻塞等待机制:无数据/缓冲区满时自动阻塞,简化同步逻辑
- 多路复用能力:select 让一个 goroutine 监听多个 channel
- 管道模式组合:多个 channel 串联实现复杂的数据流处理