共享变量必须用指针或同步原语保护,不能直接传值;全局变量不天然线程安全;只读变量可安全共享,否则须用sync.Mutex或sync/atomic等显式同步。

共享变量必须用指针或同步原语保护,不能直接传值
Go 的 goroutine 之间不共享栈,所有通过函数参数、返回值或局部变量传递的数据都是副本。如果你写 var counter int 然后在多个 goroutine 里直接读写它,就属于未同步的竞态访问——go run -race 会立刻报 Data race on variable counter。
常见错误是以为“变量声明在全局”就天然线程安全,其实不是:全局变量只是地址固定,读写操作本身仍需原子性或互斥控制。
- 不要用
counter++这类非原子操作直接修改整型变量 - 不要把结构体值(如
Config{Port: 8080})直接传给多个 goroutine 并期望它们能安全更新字段 - 如果变量只读(初始化后不再变),可以安全共享;否则必须显式同步
优先用 sync.Mutex 封装可变状态
对需要频繁读写的结构体字段(比如计数器、缓存 map、连接池状态),最直观的方式是把变量和锁一起封装进 struct:
type Counter struct {
mu sync.Mutex
n int
}
func (c *Counter) Inc() {
c.mu.Lock()
defer c.mu.Unlock()
c.n++
}
func (c *Counter) Value() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.n
}
注意:sync.Mutex 必须取地址使用(即 &Counter{}),否则每次传值都会复制一个新锁,失去互斥意义;也不要嵌入 sync.Mutex 后忘记导出字段名(比如写成 mu sync.RWMutex 却没加 exportedMu sync.RWMutex 导致无法调用 Lock())。
立即学习“go语言免费学习笔记(深入)”;
简单计数场景用 sync/atomic 更轻量
如果只是增减、比较并交换一个整数或指针(如 int64, uint32, *T),sync/atomic 是零分配、无锁、性能最高的选择:
- 用
atomic.AddInt64(&counter, 1)替代counter++ - 用
atomic.LoadInt64(&counter)替代直接读counter - 注意类型严格匹配:不能对
int调用atomic.AddInt64,32 位系统上int可能是 32 位 -
atomic.Value可安全存取任意类型指针(如*Config),但不能用于结构体字段级更新
切片和 map 必须额外同步,即使底层是引用类型
Go 的 []byte 和 map[string]int 是 header 结构(含指针、长度、容量),赋值时只拷贝 header,所以多个 goroutine 指向同一底层数组或哈希表——但这不等于线程安全。并发读写 map 会 panic:fatal error: concurrent map writes;并发读写切片元素也可能越界或读到中间态数据。
- 写 map 前必须加
sync.RWMutex(读多写少时用RWMutex比Mutex更高效) - 不要依赖
sync.Map替代常规 map + 锁——它适用于读远多于写的场景,且不支持遍历或 len(),API 也更笨重 - 切片若需并发追加(
append),必须锁住整个操作;若只读元素,且长度不变、元素不可变,可不加锁
真正容易被忽略的是:哪怕你用了 atomic 或 Mutex,只要变量逃逸到 goroutine 外部(比如作为闭包变量被捕获、或作为 channel 发送值),就得确认它的生命周期和所有权是否清晰。共享变量的本质不是“在哪声明”,而是“谁负责读写、谁保证顺序、谁决定何时销毁”。


















