多协程读写同一指针指向的数据必须加锁,因* t 本身无并发安全语义,并发读写底层值会触发 data race;应使用 sync.mutex 保护解引用操作或 atomic.value 原子发布指针。

多协程读写同一指针指向的数据必须加锁
Go 的 *T 本身不带并发安全语义,多个 goroutine 同时读写 *int 或 *MyStruct 指向的内存,会触发 data race——go run -race 会直接报错。真正要保护的是「被指针访问的底层值」,不是指针变量本身(除非你也并发修改指针值)。
常见错误场景:
- 多个 goroutine 调用
func (p *Counter) Inc() { *p++ },没加锁 → 计数丢失 - 全局
var config *Config被多个 handler 并发读写 → 配置撕裂或 panic - 把
map[string]*Item的 value 指针传给 goroutine,并发修改其字段 → race detector 报告 write at 0x…
正确做法:用 sync.Mutex 包裹对 *T 解引用后的操作:
type Counter struct {
mu sync.Mutex
val int
}
func (c *Counter) Inc() {
c.mu.Lock()
c.val++
c.mu.Unlock()
}
注意:锁粒度要覆盖所有共享字段访问;不要只锁写、放任读裸跑——除非你明确保证「初始化后只读」。
用 atomic.Value 安全发布可变指针
当需要在运行时动态更新一个全局指针(比如热加载配置),又希望读侧零开销、无锁,sync/atomic.Value 是标准解法。它保证 Store 和 Load 对任意类型指针都是原子且内存可见的。
典型误用:
- 直接赋值
config = &newConfig→ 读 goroutine 可能读到部分写入的结构体(尤其含 string/slice 字段) - 对
*string用atomic.StorePointer却没配unsafe.String→ 读到撕裂字符串(长度对但底层数组指针错乱)
正确写法:
var config atomic.Value // 存 *Config
// 写(通常在初始化或 reload 时)
config.Store(&Config{Timeout: 30})
// 读(任意 goroutine 中)
c := config.Load().(*Config)
fmt.Println(c.Timeout)
atomic.Value 不支持字段级更新,每次 Store 都是整体替换。别试图用它做计数器或状态位——那是 atomic.Int32 的事。
循环中取地址要防变量复用
for 循环里直接取 &i 或 &item,所有指针最终都指向同一个内存位置(循环变量复用),这是最隐蔽的「逻辑悬空」。
错误示例:
var ptrs []*int for i := 0; i <p>修复方式(二选一):</p>
- 在循环内声明新变量:
for i := 0; i - 用临时变量承接:
for i := 0; i
如果后续要把这些指针发给 goroutine,更要小心——闭包捕获 i 也会复用,必须用上述方式切断绑定。
避免 uintptr 中断 GC 引用链
把指针转成 uintptr 后,GC 就看不见它了。如果这时原指针变量被回收,而你还在用 uintptr 构造新指针,可能 panic 或读脏数据。
典型高危操作:
-
u := uintptr(unsafe.Pointer(&x)); runtime.GC(); p := (*int)(unsafe.Pointer(u))→x已被回收,p解引用崩溃 - 在 syscall 中传
uintptr(unsafe.Pointer(&slice[0])),但 slice 生命周期短于 syscall 调用 → 系统调用读写已释放内存
安全底线:
- uintptr 转换必须紧邻:
ptr → uintptr → *T三步连续,中间不能有函数调用、goroutine 切换、GC 可能触发点 - 若需长期持有地址,必须同时持有一个真实指针(
*T)防止 GC 回收 - 系统调用传 buffer 地址时,确保 slice 在整个 syscall 过程中有效(比如用
make([]byte, N)分配并保持引用)
真正难的不是语法,而是判断「这个指针到底该不该跨 goroutine 共享」——多数时候,用 channel 传值比共享 *T 更简单、更安全。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











