hchan是go运行时私有结构体,不可直接声明访问;其qcount对应len(ch),dataqsiz决定cap(ch),buf为环形缓冲区起始地址,sendx/recvx为环形索引,sendq/recvq挂sudog链表,lock保障串行化。

hchan 不是你能直接声明或访问的类型,它是 Go 运行时在堆上分配的私有结构体,所有 make(chan T, N) 调用背后都隐式创建了一个 hchan 实例。
hchan 的核心字段和它们的真实含义
它不是“带锁队列”的简单封装,而是一组协同工作的内存+状态+调度组件。每个字段都直接对应运行时行为,不能凭名字猜测:
-
qcount是当前buf中真实存放的元素个数,也就是len(ch)的返回值;它不包含正在 sendq/recvq 中等待交接的数据 -
dataqsiz决定cap(ch):为 0 就是无缓冲 channel,此时buf == nil,所有收发必须 goroutine 配对完成 -
buf是一块原始内存块(非 slice),只在dataqsiz > 0时有效;sendx和recvx是它的环形索引,不是数组下标 -
sendq和recvq是双向链表,节点是sudog—— 封装了 goroutine 指针、待传值地址、唤醒状态等,不是数据本身 -
lock是真实互斥锁,所有操作(包括无缓冲 channel 的 send/recv)都必须先加锁;没有“无锁 fast path”
为什么 cap(ch) - len(ch) 不是剩余空间
这个表达式在无缓冲 channel 上永远是 0 - 0,但它的语义完全错误:无缓冲 channel 根本没有“剩余空间”概念,它只表示“此刻有没有配对 goroutine 在等”。有缓冲 channel 也一样危险:
-
qcount只统计buf中的元素,不包括正被 goroutine 直接交接(绕过buf)的数据 -
select多 case 就绪时,runtime 伪随机选一个执行,len(ch)读出来瞬间就可能被另一个 goroutine 改变 - 你无法靠字段推导出“是否阻塞”,最终行为取决于
sendq/recvq是否为空、closed状态、以及当前 goroutine 是否能立刻配对
发送/接收时到底拷贝了什么
channel 从不做“引用传递”,只做值拷贝;但拷什么、拷多少,由类型决定:
-
chan int:每次拷贝 8 字节(64 位平台) -
chan *int:拷贝指针值(8 字节),不是它指向的int;接收方改*p会影响原值,这是指针语义,不是 channel 特性 -
chan []byte:拷贝 slice header(3 个 word),底层数组地址不变;若发送后立刻改底层数组内容,接收方可能看到脏数据 -
chan interface{}:拷贝 interface{} header(2 word),含类型指针和数据指针;它不避免底层值拷贝,只是把“拷哪”延迟到运行时
真正容易被忽略的是:hchan 本身分配在堆上,哪怕你的 ch 变量在栈上;sendq/recvq 中挂的 sudog 也是堆分配对象;channel 关闭后,closed 字段置 1,但 recvq 里等待的 goroutine 仍会被唤醒并收到零值——这些细节全藏在 runtime 的锁保护逻辑里,没法绕过。











