hchan结构体包含qcount、dataqsiz、buf、sendx、recvx、sendq、recvq等字段,其中qcount即len(ch),dataqsiz即cap(ch),buf仅当有缓冲时非nil,无缓冲channel的buf恒为nil。

hchan 结构体到底存了哪些字段
Go 的 hchan 是运行时中 channel 的底层表示,它不是用户可直接访问的类型,但理解它的字段组成能帮你判断 channel 行为是否符合预期。比如你看到 select 阻塞、len(ch) 返回非零却读不到数据,往往和这些字段的状态有关。
核心字段包括:qcount(当前队列中元素个数)、dataqsiz(环形缓冲区容量)、buf(指向底层数组的 unsafe.Pointer)、sendx/recvx(环形队列读写索引)、sendq/recvq(等待的 goroutine 链表)。注意:buf 仅当 dataqsiz > 0 时才非 nil;无缓冲 channel 的 buf 永远为 nil,所有通信都靠 goroutine 直接交接。
- 用
go tool compile -S看汇编,或调试时在runtime/chan.go打断点,能观察到hchan字段的实际值变化 -
qcount和len(ch)是同一字段,但cap(ch)对应的是dataqsiz,不是qcount - 别假设
buf指向的数据是连续拷贝——它只是环形缓冲区的起始地址,实际元素按sendx/recvx偏移读取
channel 发送/接收时指针怎么传的
Go 的 channel 不会复制元素本身,而是复制元素的内存块(按类型大小),对指针类型(如 *int、map[string]int)来说,“传递指针”其实是把指针值(8 字节)拷贝进 buf 或直接传给接收方 goroutine 的栈。关键点在于:**channel 传递的是值,而这个值恰好是指针**。
所以当你往 chan *int 里 send 一个 &x,真正进 buffer 的是该地址的副本;接收方拿到的也是同一个地址的副本。这和 “传递指针” 的直觉一致,但底层没有特殊指针转发逻辑——就是 memcpy。
立即学习“go语言免费学习笔记(深入)”;
- 如果发送的是大结构体(比如 1MB 的 struct),即使你用
chan *MyStruct,也只拷贝 8 字节指针,但你要确保该指针指向的内存生命周期足够长(不能是栈上局部变量) - 对
chan []byte,每次 send 都拷贝 slice header(3 个 word),不拷贝底层数组;底层数组仍由原 goroutine 管理,接收方拿到的是 header 副本 + 同一数组指针 - 不要误以为
chan interface{}能“避免拷贝”——interface{} 本身是 2-word 结构(type ptr + data ptr),传的是它的值,不是原始对象的引用
为什么 close(ch) 后还能读,但不能再写
close 操作本质是原子设置 hchan.closed = 1,并唤醒所有阻塞在 recvq 上的 goroutine。此时 sendq 中的 goroutine 会被标记为 panic(触发 panic: send on closed channel),而 recvq 中的 goroutine 会继续完成读取:先取完 buf 中剩余元素,再返回零值 + false。
这个行为由 runtime 中 chanrecv 函数控制,它检查 closed 标志后分两步处理:有数据就读,没数据就返回零值。注意:即使 qcount == 0,只要 closed == 1,非阻塞读(ch 后带 ok)也会立即返回零值和 false。
- 不要依赖 “close 后读一次就清空” —— 如果有多个 goroutine 并发读,可能都读到零值,也可能部分读到残留数据
-
range ch在 close 后自动退出,是因为它内部每次读都检查 ok,一旦为 false 就 break,不是靠检测closed字段 - close 本身不修改
buf内容,也不释放buf内存,直到整个hchan被 GC 回收
调试 hchan 时最容易忽略的内存布局细节
hchan 结构体本身是堆分配的(通过 new(hchan)),但它的 buf 字段指向的内存,取决于 channel 类型和大小:小缓冲区(如 make(chan int, 64))通常走 mcache 分配,大缓冲区可能直接 mmap;而无缓冲 channel 的 buf 永远为 nil,所有数据交换都在 goroutine 栈之间完成。
这意味着:用 pprof 看 heap profile 时,hchan 对象本身很小(约 48 字节),但它的 buf 可能占几 MB;用 dlv 查看 hchan 地址时,别直接 dereference buf,得先确认 dataqsiz 是否为 0,否则会读到非法内存。
- GC 不会单独回收
buf,它和hchan是一体管理的:只有hchan不可达时,buf才被标记为可回收 - 用
unsafe.Sizeof(hchan{})得到的是结构体大小,不是实际内存占用;真实开销要看dataqsiz * unsafe.Sizeof(T) - 调试时打印
hchan.sendq.first或hchan.recvq.first只能看到 g 结构体地址,无法直接看到 goroutine 栈帧内容——那是调度器私有数据










