Go channel 的核心行为由 hchan 结构体字段决定:qcount(当前元素数)、dataqsiz(缓冲容量)、buf(环形缓冲区指针)、sendx/recvx(环形索引)、sendq/recvq(goroutine 等待队列)和 lock(互斥锁)。

Go 的 channel 不是黑盒,它的行为完全由 hchan 结构体和 runtime 中几处关键函数(chansend、chanrecv、closechan)决定。理解它不靠猜,而靠看字段怎么用、条件怎么判、goroutine 怎么挂起和唤醒。
hchan 结构体里哪几个字段真正管事
hchan 是 runtime 内部结构,用户不可见,但它的字段直接决定 channel 行为:
-
qcount:当前缓冲区中元素个数,len(ch)返回的就是它;判断是否空/满只看它,不是看sendx == recvx -
dataqsiz:缓冲容量,cap(ch)返回它;为 0 就是无缓冲 channel -
buf:仅当dataqsiz > 0时非 nil,指向一块堆上分配的连续内存(不是 slice,是 raw memory) -
sendx/recvx:环形游标,取值范围始终在[0, dataqsiz),写满后自动归零,不参与空/满判断 -
sendq/recvq:双向链表(waitq类型),节点是sudog,封装了 goroutine 指针、待传数据地址等;不用 slice 是因为要避免 realloc 和 GC 干扰
无缓冲 channel 的 buf == nil、dataqsiz == 0,所有通信都走 sendq/recvq 配对唤醒路径,根本不动缓冲区。
发送阻塞的三个明确触发点
向 channel 发送不一定会挂起 goroutine,只有满足以下任一条件才阻塞:
-
ch == nil:直接 panic:panic: send on nil channel -
cap(ch) == 0(无缓冲)且recvq.first == nil(没人等着收) -
cap(ch) > 0但qcount == cap(ch)(缓冲已满)且recvq.first == nil(也没人等着收)
注意:即使缓冲未满,只要此刻有 goroutine 卡在 recvq 上,send 会跳过 buf,直接把数据拷贝过去并唤醒对方——这是“配对转发”,不是“先存再取”。误以为“有空位就不阻塞”是常见逻辑漏洞。
接收操作为什么有时返回零值却不 panic
接收行为高度依赖写法和 channel 状态:
v := :若qcount > 0,从buf[recvx]拷贝并更新recvx;否则阻塞进recvq;若ch已关闭且qcount == 0,立刻返回零值(不阻塞)v, ok := :这是唯一能安全探测关闭状态的方式;ok == false仅表示“channel 已关且缓冲已空”,不是“读错了”
典型错误是只写 v := 去读一个可能已关闭的 channel,结果持续拿到零值(比如 0、nil、""),逻辑 silently 错误。更隐蔽的是:close 后还能读到缓冲区剩余数据,ok 只在缓冲耗尽后才变 false。
select 中多个 case 都 ready 时到底选谁
没有顺序保证。当多个 case 都可执行(比如多个 channel 都有数据可读、或都未满可写),runtime 会伪随机选择一个,既不是按代码顺序,也不是按就绪先后。
- 这意味着:
default放最后 ≠ “兜底”,它和其他case平等竞争 -
select中所有 channel 操作都是非阻塞检测;一旦有任意一个 ready,其余不再评估 - 若需确定顺序(如优先读 A,A 没数据再读 B),必须拆成两个独立
select+default,或加锁协调
这个随机性是故意设计的,防止隐式依赖顺序引发竞态或饥饿——但恰恰因为“不保证”,很多人在线上看到 case 执行顺序漂移时第一反应是怀疑调度器 bug,其实只是没意识到这是语言契约的一部分。











