决定channel是否带缓冲的关键字段是dataqsiz、buf和qcount:dataqsiz为0时为无缓冲channel,buf仅当dataqsiz>0时非nil,qcount表示当前队列元素数且≤dataqsiz。

hchan 结构体里哪些字段决定 channel 是否带缓冲
关键就看 qcount、dataqsiz 和 buf 这三个字段:dataqsiz 是用户声明的缓冲区大小,为 0 就是无缓冲 channel;buf 指向底层循环队列内存,只有 dataqsiz > 0 时才非 nil;qcount 是当前队列中实际元素个数,它永远 ≤ dataqsiz。
常见错误是以为 len(ch) 能反映“剩余容量”,其实它返回的是 qcount(已存元素数),而剩余空间得用 cap(ch) - len(ch) 算——但注意 cap(ch) 对无缓冲 channel 返回 0,别拿它做减法。
- 无缓冲 channel 的
buf == nil且dataqsiz == 0,所有收发必须同步配对,否则阻塞 - 有缓冲 channel 的
buf在 make 时按dataqsiz * elem.size分配,不随元素类型变化而重分配 - 如果
dataqsiz很大但元素很大(比如[1024]byte),buf会一次性申请巨量内存,容易触发栈溢出或 GC 压力
send 函数如何判断是否能立即写入
核心逻辑在 chansend 中:先检查 qcount ,成立就直接拷贝进 <code>buf 并更新 qcount 和尾指针 sendx;否则进入阻塞流程。这里没有“尝试写入失败就返回 false”的选项——Go channel 不支持非阻塞写(除非用 select + default)。
容易踩的坑是误以为 ch 会像 CSP 里的 alt 那样自动跳过满 channel,实际上它要么成功,要么挂起 <a style="color:#f60; text-decoration:underline;" title="go" href="https://www.php.cn/zt/15863.html" target="_blank">go</a>routine 并加入 <code>sendq 等待被接收方唤醒。
立即学习“go语言免费学习笔记(深入)”;
- 写入时若
elem类型含指针,runtime 会把值完整拷贝进buf,不是存指针——所以接收方拿到的是副本,修改不影响发送方 - 如果 sender 被挂起,它的 goroutine 会被标记为
waiting状态,并链入hchan.sendq,由 receiver 在 recv 后调用goready唤醒 - 注意:即使
buf未满,若此时有 goroutine 正在recv并阻塞在recvq上,send会跳过缓冲区,直接把数据拷贝给 receiver,不经过buf
close(chan) 后还能不能 recv 和 send
close 只改 hchan.closed 字段为 1,不碰 buf 或队列指针。之后:send 立即 panic "send on closed channel";recv 则分情况——若 qcount > 0,正常取走一个元素并返回 true;若 qcount == 0,返回零值 + false。
典型误用是假设 “close 后 recv 一定得到零值”,其实只要缓冲区还有剩,就能继续取到有效值。更隐蔽的问题是:多个 goroutine 并发 close 同一个 channel 会 panic,而 Go 不提供原子 close 检查机制。
- receiver 无法区分 “channel 已关且无数据” 和 “channel 没关但暂时没数据”,都表现为
val, ok := 中 <code>ok == false - 不要依赖
len(ch) == 0 && cap(ch) == 0来判断是否关闭——这是无效的,关闭状态只能靠接收时的ok或 recover panic - 关闭前务必确保没有其他 goroutine 正在执行
ch ,否则 race detector 会报数据竞争
为什么 hchan.buf 是 unsafe.Pointer 而不是 []byte
因为 buf 要承载任意类型的元素,而 Go 的 slice 有类型信息和长度字段,无法在编译期确定布局。用 unsafe.Pointer 配合 dataqsiz 和 elemsize,才能在运行时做偏移计算和内存拷贝——比如定位第 i 个元素就是 (*elemType)(add(buf, uintptr(i*elemsize)))。
这也解释了为什么 channel 不能直接用反射操作缓冲区:reflect 包无法安全地把 unsafe.Pointer 转成可寻址的 slice,强行转换会导致 GC 无法追踪指针,引发悬垂引用。
- 每次读写
buf都要算偏移,所以sendx和recvx是 uint,不是 int——避免负偏移越界 -
buf内存来自堆,不受栈逃逸分析影响;但若dataqsiz为 0,整个hchan结构体可能分配在栈上(取决于逃逸分析结果) - 调试时想看
buf内容?pprof 或 delve 都没法直接 dump,得靠readmem+elemsize手动解析,这也是源码难啃的一环










