17370845950

Golang指针与channel结合使用_Golangchannel传递指针数据
传指针本身不会引发数据竞争,真正危险的是多个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:适合大结构体(如含 []bytemap、嵌套指针的结构),避免拷贝开销;或需要接收方能更新原始对象(如任务结果回填)
  • 注意:chan []*Tchan *[]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 更高效。

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 类型声明。