hchan结构体分配在堆上,ch是栈上指针但指向堆中hchan;buf为unsafe.Pointer指向裸内存块;sendx/recvx是模dataqsiz的回绕索引;sendq/recvq是受锁保护的sudog双向链表。

hchan 结构体本身分配在堆上,不是栈变量
你声明 ch := make(chan int, 10) 时,ch 是一个栈上的指针变量,但它指向的 hchan 实例一定在堆上。这是因为 channel 要被多个 goroutine 并发访问,而栈内存无法跨 goroutine 共享——运行时会直接调用 newobject 在堆上分配 hchan。
这带来两个实际影响:
- 即使你在函数内创建 channel,它也不会随函数返回而销毁;
- 用
go tool compile -S看汇编,能看到call runtime.mallocgc,而不是栈分配指令。
buf 字段不是 slice,而是 raw 内存块指针
hchan.buf 类型是 unsafe.Pointer,它指向一块通过 mallocgc 分配的、大小为 dataqsiz * elemsize 的连续内存,不是 Go 的 []byte 或其他 slice。这意味着:
- 这块内存不带
len/cap头,也不参与 slice 的逃逸分析逻辑; - 读写完全靠
sendx和recvx手动计算偏移,例如第i个元素地址是add(buf, i*elemsize); - 无缓冲 channel 的
buf恒为nil,此时所有收发都绕过 buf,直接走 goroutine 交接。
sendx/recvx 是回绕索引,不是数组下标
sendx 和 recvx 都是 uint,取值范围严格在 [0, dataqsiz) 内,每次递增后都会模 dataqsiz(但实际实现用的是条件判断+归零,避免除法开销)。关键点在于:
- 它们不表示“已发送/已接收总数”,只表示当前环形缓冲区中的读写位置;
-
qcount == 0表示空,qcount == dataqsiz表示满,不依赖(sendx - recvx) % dataqsiz计算——因为qcount是独立维护的计数器; - 调试时若看到
sendx == 5且dataqsiz == 5,说明下一次写会回到 0,不是越界。
sendq/recvq 是双向链表,节点是 sudog
sendq 和 recvq 是 runtime.waitq 类型,本质是带 first/last 指针的双向链表,每个节点是 sudog 结构体。这个设计直接影响阻塞行为:
-
sudog里存了 goroutine 的指针、待发送/接收值的地址、是否已唤醒等状态,不是简单挂起 ID; - 当 channel 关闭时,
recvq中所有sudog会被唤醒并返回零值,而sendq中的会被标记 panic; - 注意:链表操作受
hchan.lock保护,但 goroutine 状态变更(如从 waiting → runnable)必须在锁外完成,否则可能死锁。
实际写代码时很少需要碰 hchan,但一旦遇到 select 死锁、len(ch) 不为零却读不到数据、或 pprof 显示大量 goroutine 卡在 chan send,就得意识到:你正在和这几个字段的真实状态打交道。它们不是抽象概念,而是内存里几个字节的值。











