引言
Golang 通过内置的 chan
类型为并发编程提供了优雅的通信和同步手段。相比于传统的锁(mutex
)和条件变量(cond
),channel 的设计更符合 Go “不要通过共享内存来通信,而要通过通信来共享内存”(“Don’t communicate by sharing memory; share memory by communicating”)的理念。本文将从使用角度出发,结合底层实现机制,深入剖析 Go 中的 channel。
一、Channel 基础
- 定义与声明
// 声明一个只能发送 int 的 channel ch := make(chan int) // 带缓冲区的 channel,容量为 5 bufCh := make(chan string, 5)
- 发送与接收
// 发送(若缓冲区已满或无缓冲,则阻塞) ch <- 42 // 接收(若缓冲区为空或无缓冲,则阻塞) v := <-ch // 同时获取值和判断是否关闭 v, ok := <-ch
- 关闭 Channel
close(ch) // 关闭后还能接收剩余数据,但再发送将 panic
二、Channel 的底层实现机制
Go 语言的通道在运行时由 runtime.hchan
结构体表示,其核心字段如下(Go 1.22.6 源码摘选):
type hchan struct {
qcount uint // 当前队列中元素数
dataqsiz uint // 缓冲区大小
buf unsafe.Pointer // 指向元素缓冲区的指针
elemsize uint16 // 单个元素大小
closed uint32 // 是否已关闭
elemtype *_type // 元素类型
sendx uint // 生产者索引
recvx uint // 消费者索引
recvq waitq // 接收者等待队列
sendq waitq // 发送者等待队列
lock mutex // 保护 hchan 结构体
}
- 缓冲与索引
buf
指向一段连续内存,长度为dataqsiz * elemsize
。sendx
/recvx
分别为写和读的位置索引,循环使用。qcount
记录当前缓冲中剩余的元素数。
- 等待队列(waitq)
- 当发送者或接收者因无缓冲或缓冲区满/空而需要阻塞时,会被挂入
sendq
或recvq
,本质是一个链表,节点类型为sudog
,包含指向等待 goroutine 的指针。 - 当一个发送或接收操作可以完成时,运行时会唤醒对端队列头部的 goroutine。
- 当发送者或接收者因无缓冲或缓冲区满/空而需要阻塞时,会被挂入
- 发送流程(简化)
chanSend: lock(&hchan.lock) if channel closed → panic if recvq 非空: dequeue 一个等待接收者,将数据直接拷贝给它,唤醒该 goroutine else if qcount < dataqsiz: 将数据写入 buf[sendx], sendx = (sendx+1)%dataqsiz, qcount++ else: 将当前 goroutine 挂入 sendq,然后 unlock 并 park(阻塞) unlock(&hchan.lock)
- 接收流程(简化)
chanRecv: lock(&hchan.lock) if sendq 非空: dequeue 一个等待发送者,直接从它那里拷贝数据,唤醒该 goroutine else if qcount > 0: 从 buf[recvx] 读数据, recvx = (recvx+1)%dataqsiz, qcount-- else if channel closed: 返回零值并标记 ok=false else: 将当前 goroutine 挂入 recvq,然后 unlock 并 park(阻塞) unlock(&hchan.lock)
- 关闭 Channel
close(ch)
将closed
字段设为 1,并唤醒recvq
中所有等待者,让它们尽快返回零值;向已关闭 channel 发送会直接 panic。
三、Channel 的使用场景
- Goroutine 同步
done := make(chan struct{}) go func() { // 执行耗时操作... close(done) }() <-done // 等待子 goroutine 完成
- Pipeline 模式
将任务分成多个阶段,用 channel 串联起来,形成数据流水线。// 1. 生成器 gen := func(nums ...int) <-chan int { out := make(chan int) go func() { for _, n := range nums { out <- n } close(out) }() return out } // 2. 计算器 sq := func(in <-chan int) <-chan int { out := make(chan int) go func() { for n := range in { out <- n*n } close(out) }() return out } // 使用 in := gen(2,3,4) out := sq(in) for v := range out { fmt.Println(v) }
- Fan-In / Fan-Out
- Fan-Out:把同样的输入分发给多个 worker
- Fan-In:把多个 worker 的输出合并到一个 channel
// 合并多个 channel func merge(cs ...<-chan int) <-chan int { out := make(chan int) var wg sync.WaitGroup wg.Add(len(cs)) for _, c := range cs { go func(c <-chan int) { defer wg.Done() for v := range c { out <- v } }(c) } go func() { wg.Wait() close(out) }() return out }
- 超时与
select
select { case res := <-ch: fmt.Println("收到:", res) case <-time.After(time.Second * 2): fmt.Println("超时") }
- Worker Pool(协程池)
jobs := make(chan int, 100) results := make(chan int, 100) // 启动 N 个 worker for w := 0; w < 5; w++ { go func() { for j := range jobs { results <- doWork(j) } }() } // 投递任务 for i := 0; i < 20; i++ { jobs <- i } close(jobs) // 收集结果 for i := 0; i < 20; i++ { fmt.Println(<-results) }
四、性能与注意事项
- 无缓冲 vs 有缓冲
- 无缓冲 channel 在发送和接收之间做同步,适合严格的点对点同步。
- 有缓冲 channel 在缓冲区未满/空时不会阻塞,可提高吞吐,但也可能导致 goroutine 泄漏(未及时关闭或接收)。
- 避免死锁
- 从已关闭或未打开的 channel 接收可能导致死锁。
- 在使用
select
时,务必处理所有分支(包括default
或超时)。
- 关闭 channel
- 只有发送方应关闭 channel;接收方只负责读取。
- 多个发送者要避免重复关闭。
五、总结
Go 的 channel 不仅是并发通信的核心抽象,其底层通过 hchan
、等待队列、原子操作等机制,实现了高效且安全的阻塞/唤醒流程。掌握 channel 的内部原理,有助于在高并发场景下编写更可靠、更健壮的程序。配合 select
、pipeline、worker pool 等模式,channel 能助力你优雅地构建复杂的并发系统。

声明:本作品采用署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0)进行许可,使用时请注明出处。
Author: mengbin
blog: mengbin
Github: mengbin92
cnblogs: 恋水无意
腾讯云开发者社区:孟斯特
—