引言
Golang 通过内置的 chan 类型为并发编程提供了优雅的通信和同步手段。相比于传统的锁(mutex)和条件变量(cond),channel 的设计更符合 Go “不要通过共享内存来通信,而要通过通信来共享内存”(“Don’t communicate by sharing memory; share memory by communicating”)的理念。本文将从使用角度出发,结合底层实现机制,深入剖析 Go 中的 channel。
// 声明一个只能发送 int 的 channel
ch := make(chan int)
// 带缓冲区的 channel,容量为 5
bufCh := make(chan string, 5)
// 发送(若缓冲区已满或无缓冲,则阻塞)
ch <- 42
// 接收(若缓冲区为空或无缓冲,则阻塞)
v := <-ch
// 同时获取值和判断是否关闭
v, ok := <-ch
close(ch)
// 关闭后还能接收剩余数据,但再发送将 panic
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 记录当前缓冲中剩余的元素数。sendq 或 recvq,本质是一个链表,节点类型为 sudog,包含指向等待 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)
close(ch) 将 closed 字段设为 1,并唤醒 recvq 中所有等待者,让它们尽快返回零值;向已关闭 channel 发送会直接 panic。done := make(chan struct{})
go func() {
// 执行耗时操作...
close(done)
}()
<-done // 等待子 goroutine 完成
// 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)
}
// 合并多个 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("超时")
}
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)
}
select 时,务必处理所有分支(包括 default 或超时)。Go 的 channel 不仅是并发通信的核心抽象,其底层通过 hchan、等待队列、原子操作等机制,实现了高效且安全的阻塞/唤醒流程。掌握 channel 的内部原理,有助于在高并发场景下编写更可靠、更健壮的程序。配合 select、pipeline、worker pool 等模式,channel 能助力你优雅地构建复杂的并发系统。
声明:本作品采用署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0)进行许可,使用时请注明出处。
Author: mengbin
blog: mengbin
Github: mengbin92
cnblogs: 恋水无意
腾讯云开发者社区:孟斯特
—