<p>Go中chan T传递指针仅复制地址(8字节),虽避免结构体拷贝,但若接收方无锁且无生命周期检查地读写 T指向数据,将引发竞态或悬垂访问。</p>

channel 传 *T 不会额外拷贝指针值,但容易误判“零开销”
Go 中 chan *T 的每次发送/接收,只复制一个机器字长的地址(通常 8 字节),远小于传大结构体 T 的开销。但这不等于“没成本”——真正隐患在后续使用环节。很多人看到“没拷贝结构体”就以为安全高效,结果在接收方直接读写 *T 指向的数据时,既没锁、也没生命周期检查,埋下竞态或悬垂访问。
常见错误现象:go func() { 中的 <code>u 是从 channel 收到的指针,但发送方早已 return,栈帧销毁,u 指向内存已被复用。
- 函数内局部变量取地址后发到 channel(如
&item在 for 循环中),接收方拿到的是同一栈地址,循环迭代后内容已变 - 用
sync.Pool复用含指针字段的结构体,归还前未清空内部指针,下次取出后解引用旧地址 - HTTP handler 启动 goroutine 并传入
r.Context().Value("db").(*DB),但请求结束、context cancel 后该*DB可能被回收或置为无效
slice/map 指针通过 channel 传递时,底层数组可能被意外覆盖
这不是 channel 本身的问题,而是 slice 和 map 的底层结构导致:它们都包含指向底层数组或哈希表的指针字段。当你传 *[]byte 或 *map[string]int,接收方拿到的是指针,但若发送方后续调用 append 或增删 map key,底层数组可能 realloc,原指针指向的内存失效或内容错乱。
典型踩坑代码:data := make([]byte, 0, 32); ch —— 此时 <code>&data 指向的 slice header 还在,但其 ptr 字段可能已在 append 中被更新,接收方读到的是旧数组残留或 panic。
- 避免传
*[]T或*map[K]V,改用传值[]T或map[K]V(小数据)或封装成只读接口 - 若必须传指针,确保发送方不再修改底层数组(如用
data[:len(data):cap(data)]锁定容量,禁止 realloc) - 对 map 操作,优先用
sync.Map替代裸指针 +map组合
接收方未校验指针有效性就解引用,触发 panic 或未定义行为
channel 传输指针不保证它非 nil,也不保证它指向有效内存。接收方若直接写 user.Name = "x" 而不做 if user != nil 判断,一旦发送方发了 nil,就会 panic: invalid memory address or nil pointer dereference。
更隐蔽的是:指针非 nil,但所指对象已被 GC 回收(比如从 unsafe 构造或绕过 runtime 管理),此时解引用行为未定义,race detector 也捕获不到。
- 所有从 channel 收到的
*T,解引用前必须显式判空:if p != nil { ... } - 不要依赖
defer free(p)类似逻辑来管理指针生命周期;Go 没有析构语义,应由发送方确保对象存活期覆盖整个 channel 生命周期 - 对敏感场景(如系统级服务),启用
go run -race测试,但它只能捕获实际发生的竞争,无法预防逻辑上“该加锁却没加”的情况
性能优化的边界:什么时候该传指针,什么时候该传值?
传 *T 的收益只在 unsafe.Sizeof(T) > 机器字长 且接收方需修改原始数据时成立。如果 T 是小结构体(如两个 int)、或接收方只读不写、或你本就要加锁同步,那传值反而更清晰安全。
例如 type Point struct{ X, Y int }(16 字节),传值比传指针多 8 字节拷贝,但省去了锁、生命周期跟踪和空指针检查——综合看,往往更稳。
- 结构体大小
- 结构体含
sync.Mutex或其他不可拷贝字段:必须传指针,否则编译失败 - 接收方只读且无并发写:传值 +
const语义更易推理,避免意外修改
最常被忽略的一点:channel 本身不是内存管理器。它不管指针指向哪,也不管对象活多久。你得自己画清楚数据所有权边界——谁分配、谁释放、谁读、谁写、何时失效。一旦把这个想成“channel 帮我管引用”,问题就来了。



















