chan 是值类型,但变量存储的是指向堆上 hchan 结构体的隐藏指针;赋值或传参时复制该指针,故能共享底层队列,但无法获取或修改此指针。

Go 通道 chan 本身不是指针,但底层数据结构是堆上分配的
很多人看到 chan 能在函数间“共享”、不显式传指针就以为它是引用类型,甚至误以为 chan 变量存的是指针值。其实 chan 是 Go 的内置类型,变量本身是值类型(类似 map 或 func),但它的底层结构体(hchan)一定分配在堆上,且变量只保存指向它的指针——这个指针对用户完全透明。
这意味着:chan 变量赋值、传参时复制的是这个隐藏指针,不是整个队列数据。所以两个变量能操作同一底层队列,但你永远拿不到、也不能修改这个指针本身。
- 常见错误现象:
nil chan发送或接收会永久阻塞,不是 panic;这恰恰说明它不是普通指针(否则解引用会 panic) - 使用场景:跨 goroutine 通信时,直接传
chan int就够了,不用写*chan int——后者语法非法,Go 不允许取通道地址 - 性能影响:小容量无缓冲通道(如
make(chan struct{}, 0))几乎不占额外内存,但底层结构体仍需堆分配,频繁创建销毁有 GC 压力
为什么不能对 chan 取地址或声明 *chan
Go 编译器禁止所有对 chan 类型取地址的操作,包括 &ch、new(chan int)、var p *chan int。这不是限制,而是设计选择:通道的生命周期和同步语义必须由运行时严格管控,暴露底层指针会破坏 channel 的原子性保证(比如关闭状态、发送/接收计数等)。
- 常见错误现象:
cannot take the address of ch—— 这不是 bug,是编译器在拦住你绕过 runtime 管理 - 参数差异:函数参数写
func worker(ch chan int)和func worker(ch 都是传“通道句柄”,区别只在方向约束,不是传址/传值之分 - 兼容性影响:Go 1 兼容承诺明确说
chan类型行为稳定,任何试图通过反射或 unsafe 获取其内部指针的操作都属未定义行为,不同版本可能崩溃
chan 传参时到底发生了什么
当把一个 chan 变量传给函数,实际复制的是一个固定大小(通常 8 字节)的句柄,里面封装了指向 hchan 结构体的指针、类型信息和一些标志位。这个句柄本身不可见,也不可操作。
立即学习“go语言免费学习笔记(深入)”;
- 实操建议:别写
func f(c *chan int)—— 编译不过;也别为了“节省复制”而刻意用全局变量或闭包捕获通道,该传就传,没开销 - 使用场景:worker 模式中,多个 goroutine 共享同一个
chan int参数是标准做法,无需额外同步 - 容易踩的坑:如果函数内对通道做了
close(ch),调用方再发数据会 panic;但关闭动作本身不需要加锁,因为close是原子的——前提是别在多个 goroutine 里并发 close 同一个通道
什么时候真需要指针?只有包装通道时才用
如果你要封装通道逻辑(比如带超时重试的管道、带统计的通道代理),那你会定义结构体,里面字段是 chan 类型,这时结构体本身可能需要指针接收者,但通道字段仍是值类型。
- 示例:
type Pipe struct { ch chan int } func (p *Pipe) Send(x int) { p.ch <- x } // 接收者用指针,是因为想复用 p.ch,不是因为 ch 本身要指针 - 关键点:这里的
*Pipe是为了方法能修改结构体其他字段(比如计数器),和ch字段是否指针无关;p.ch仍是那个隐藏指针的副本 - 容易被忽略的地方:结构体里存
chan时,如果结构体本身是栈上分配(比如局部变量),通道底层仍在堆上——GC 会跟踪到它,不会因结构体消失而提前回收通道
事情说清了就结束










