go用goroutine实现并发,但多goroutine读写共享变量需同步,否则必现竞态;sync.mutex用于互斥,rwmutex优化读多写少场景,atomic提供无锁原子操作,channel可实现无锁通信,开发时须开启-race检测并遵循“不共享优先、共享必同步”原则。

Go 语言本身没有“多线程”概念,而是用轻量级的 goroutine 实现并发。但资源竞争问题和加锁逻辑,与传统多线程完全一致——多个 goroutine 同时读写同一变量,若无同步机制,就会出现结果错乱、数据丢失等不可预测行为。
为什么必须加锁?核心是避免竞态条件
像 counter++ 这样的操作,在底层实际分三步:读取当前值 → 加 1 → 写回内存。如果两个 goroutine 几乎同时执行这三步,就可能都读到旧值、各自加 1、再写回去,最终只加了一次。这不是概率问题,而是确定会发生——只是每次调度顺序不同,导致错误表现不固定。
典型表现包括:
- 计数器最终值小于预期(如启动 1000 个 goroutine 自增,结果却只有 982)
- 结构体字段部分更新,其他字段仍是旧值
- 程序偶尔 panic 或返回空/零值,重启后又正常
sync.Mutex:最常用、最直接的互斥锁
它保证同一时刻只有一个 goroutine 能进入临界区。使用要点很明确:
- 加锁后必须配对解锁,否则其他 goroutine 永远阻塞
- 推荐用
defer mu.Unlock(),确保函数退出时一定释放(含 panic 场景) - 锁的粒度要小:只包裹真正需要保护的共享操作,不要把日志、HTTP 请求、文件读写等耗时操作包进去
示例片段:
var mu sync.Mutex
var counter int
func increment() {
mu.Lock()
defer mu.Unlock()
counter++
}
sync.RWMutex:读多写少场景的性能优化选择
当共享数据被大量读取、极少修改时(比如配置缓存、白名单列表),RWMutex 比普通 Mutex 更高效:
-
RLock()和RUnlock():允许多个 goroutine 同时读,不互斥 -
Lock()和Unlock():写操作独占,会阻塞所有新读锁和写锁 - 注意:写锁升级(先读再写)必须手动释放读锁,再获取写锁,不能直接“升”
不一定要锁:原子操作与无锁设计
对于简单整型或指针的增减、比较交换等,sync/atomic 包提供零锁开销的原子指令:
-
atomic.AddInt64(&counter, 1)替代加锁自增 -
atomic.LoadInt64(&counter)安全读取 -
atomic.CompareAndSwapInt64(&counter, old, new)实现乐观锁逻辑
更进一步,可通过 channel 将共享状态的操作串行化(如用一个 goroutine 专管计数器,其他 goroutine 发送增减指令),彻底规避锁和竞态。
辅助手段:检测 + 设计习惯
光靠经验容易漏掉竞态。开发阶段务必开启竞态检测:
- 运行
go run -race main.go或go test -race - 构建时加
-race标志生成带检测能力的二进制 - 日志中一旦出现
WARNING: DATA RACE,立刻定位修复
设计上坚持两个习惯:
- 优先考虑“不共享”:用参数传值、channel 通信代替全局变量
- 共享即加锁(或原子/通道):只要变量被两个以上 goroutine 访问且含写操作,就必须同步
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











