直接传指针进goroutine易出错,因循环变量复用导致所有goroutine共享同一地址;sync.Mutex不保护指针本身而只保护临界区内的解引用操作;unsafe.Pointer绕过GC跟踪易致悬垂指针;channel传指针需明确所有权与生命周期。

为什么直接传指针进 goroutine 容易出错
因为 goroutine 启动是异步的,如果循环中用 &v 取地址并启动 goroutine,所有 goroutine 最终可能都指向同一个内存地址(即循环变量 v 的最后一轮值)。
- 典型现象:
for _, v := range items { go func() { fmt.Println(v) }() }输出全是最后一个v - 根本原因:Go 中
range复用循环变量,&v总是取同一块栈地址 - 修复方式:在循环体内显式拷贝变量,比如
go func(val int) { fmt.Println(val) }(v),或用局部变量val := v; go func() { ... }(val) - 注意:即使传的是结构体指针,只要它指向的是循环内反复赋值的变量,问题依旧存在
sync.Mutex 保护指针所指向的数据,不是保护指针本身
指针变量本身是可复制的,sync.Mutex 不能靠“锁住指针”来保证并发安全;必须确保所有对指针所指向内存的读写,都在临界区内完成。
- 错误写法:
var p *int; mu.Lock(); p = &x; mu.Unlock()—— 这只锁了赋值动作,后续通过p读写仍不安全 - 正确做法:把所有解引用操作(
*p)包进mu.Lock()/Unlock()区间 - 常见陷阱:把 mutex 放在结构体里但没导出,外部无法调用
Lock();或误以为 “指针+mutex 字段” 就自动线程安全 - 性能提示:避免在 hot path 中频繁加锁解引用;考虑用
sync/atomic操作简单类型(如*int32),但仅限原子读写,不支持复合操作
unsafe.Pointer 转换指针类型时丢失类型安全与 GC 可见性
在并发场景下混用 unsafe.Pointer 和普通指针,容易导致 GC 误判对象存活状态,引发悬垂指针或提前回收。
- 典型错误:
p := &x; up := unsafe.Pointer(p); ... go func() { *(*int)(up) = 42 }()—— 若x是局部变量且函数已返回,up指向的内存可能被回收 - GC 不跟踪
unsafe.Pointer,也不会将其视为根对象;一旦没有其他 Go 指针指向该内存,就可能被清理 - 并发下更危险:一个 goroutine 正在用
unsafe访存,另一个触发 GC,且无强引用维持对象生命周期 - 替代方案:优先用
sync.Pool管理临时对象;必须用unsafe时,确保原始 Go 指针在整个使用周期内持续存活(例如全局变量、堆分配对象)
channel 传递指针需明确所有权和生命周期边界
通过 channel 发送指针,本质是传递内存地址,接收方获得的是同一份数据的访问权。谁负责释放?是否允许并发读写?必须由设计者约定清楚。
立即学习“go语言免费学习笔记(深入)”;
- 常见误用:发送指向栈变量的指针(如
func() { x := 42; ch ),接收方收到后x已失效 - 安全前提:指针必须指向堆分配对象(如
new(T)或&struct{}),且生命周期长于 channel 传输和消费过程 - 并发风险:多个 goroutine 从 channel 收到同一指针后同时修改,等价于裸指针共享,仍需额外同步机制
- 建议:除非有明确性能收益(如大结构体),否则优先传值;若必须传指针,配合
sync.Once或文档注明“该指针仅由接收方独占使用”
指针本身不并发安全,安全与否取决于你怎么用它——尤其是它指向哪儿、谁在什么时候访问、有没有其他 goroutine 在同一时间做别的事。最容易被忽略的,是那些看起来“只是读一下”的解引用操作,在并发路径里也可能成为竞态源头。











