传指针本身不会引发数据竞争,真正危险的是多个goroutine通过各自持有的 T同时读写同一T实例;只读安全,读写需加锁或原子操作;chan T适合大结构体或需共享修改,chan T适合小结构体且天然线程安全。

channel 传指针会不会引发数据竞争
会,但不是因为“传了指针”本身,而是因为多个 goroutine 同时读写同一块堆内存且无同步控制。chan *T 只是把指针值发过去,接收方拿到的是该指针的副本——和 func f(p *T) 传参逻辑一致。真正危险的是:多个 goroutine 通过各自持有的 *T 去修改同一个 T 实例。
- 如果只读不写(如日志打印、计算只读字段),
chan *T安全 - 如果某 goroutine 写入
*T字段,其他 goroutine 同时读该字段,必须加sync.Mutex或改用原子操作 - 避免在发送后继续修改原结构体:发送前锁定,或发送深拷贝(
chan T更稳妥)
用 chan *T 还是 chan T?看场景
选哪种取决于数据大小、是否需共享状态、以及是否允许接收方修改原始数据。
-
chan T:适合小结构体(如type Point struct{X,Y int}),语义清晰,天然线程安全(复制传递) -
chan *T:适合大结构体(如含[]byte、map、嵌套指针的结构),避免拷贝开销;或需要接收方能更新原始对象(如任务结果回填) - 注意:
chan []*T和chan *[]T是两回事——后者是指向切片头的指针,极易因底层数组扩容导致意外共享
常见错误:关闭 channel 后仍向 *T 写入
关闭 channel 仅表示“不再发新数据”,不影响已发出的指针有效性。但若 sender 在 close 后还修改它指向的值,receiver 可能读到脏数据。
ch := make(chan *int, 1) x := 42 ch <- &x close(ch) // ✅ 关闭合法 x = 99 // ⚠️ 危险!receiver 可能刚读到 &x 就看到 x=99
- 发送前冻结数据:用
const、不可变结构体,或发送前copy到只读缓冲区 - 用
sync.Once确保初始化只做一次,避免多 goroutine 写同一*T - 更可靠的做法:发送前用
atomic.StorePointer记录地址,receiver 用atomic.LoadPointer读取,配合内存屏障
实际例子:worker 模式中复用大对象
当 worker 需反复处理不同数据但初始化开销大(如带缓存的解析器),用 chan *Parser 复用实例比每次 new 更高效。
立即学习“go语言免费学习笔记(深入)”;
type Parser struct {
cache map[string]int
buf []byte
}
func worker(ch chan *Parser) {
for p := range ch {
// 复用 p.cache,清空 buf 但不重建 map
p.buf = p.buf[:0]
// ... 解析逻辑
}
}
func main() {
ch := make(chan *Parser, 1)
go worker(ch)
p := &Parser{cache: make(map[string]int)}
ch <- p // 传指针,避免重复 alloc cache
close(ch)
}
这里关键是:worker 不修改 p.cache 的指针值,只读写其内容;且没有其他 goroutine 同时往 p.cache 写——否则仍需锁。
最易被忽略的一点:指针传递不等于自动线程安全,安全边界永远由你对内存的访问方式决定,而不是 channel 类型声明。










