1. 竞争条件(Race Condition)
是什么
多个 Goroutine 同时读写同一内存变量,且**没有同步机制**,导致最终结果取决于执行时序。
典型表现
var count int
func inc() { count++ } // 不是 atomic 操作!
goroutine1: count++
goroutine2: count++
// 结果可能是 1,而不是 2开发何时需要
共享全局状态(计数器、配置、状态机、任务队列)
缓存层(map、slice)被多个协程访问
分布式日志/指标上报的本地聚合逻辑
多实例共享文件句柄、连接池对象
如何规避
- 使用 sync.Mutex / sync.RWMutex
- 使用 sync/atomic(计数器、标志位)
- 使用 channels 传参传结果(避免共享)
- 语言内置检测go test -race(必须引入)
2. 互斥锁(Mutex)
是什么
sync.Mutex:独占锁,同一时刻只有一个 Goroutine 持有锁。
典型用法
var mu sync.Mutex
func Inc() {
mu.Lock()
count++
mu.Unlock()
}何时使用
- 少量共享数据,需要全量原子性更新
- 临界区短,锁竞争可控
- 不适合高频读多写少(用读写锁)
注意事项
- 避免在锁内调用阻塞 IO、网络请求(会扩大阻塞范围)
- 避免嵌套锁(易死锁)
- 考虑用 atomic.Valuesync.Map 替代简单 map
3. 读写锁(RWMutex)
是什么
sync.RWMutex:允许多个并发读,但写时独占。
何时使用
- 读多写少:配置缓存、只读数据结构、KVS 缓存
- 需要同时支持批量读取与更新
- 避免频繁锁竞争
典型用法
var mu sync.RWMutex
func Get(k string) string {
mu.RLock()
v := cache[k]
mu.RUnlock()
return v
}
func Set(k string, v string) {
mu.Lock()
cache[k] = v
mu.Unlock()
}注意事项
- 读操作要尽量短(避免持有 RLock 做网络 IO)
- 写操作要尽量短
- 对于高频更新的数据,重新评估是否需要 atomic + 分段锁/无锁结构
4. 内存同步(内存可见性)
本质
Go 的内存模型允许编译器/硬件**重排序、缓存刷新策略**,导致无同步机制时,一个 Goroutine 的写入,另一个可能“看不到”。
同步手段
- Mutex.Lock()/Unlock():保证释放锁时的写对其他持有过锁的 Goroutine 可见
- sync/atomic 提供“加载-使用-存储”的内存屏障语义(如 atomic.StorePointer / atomic.LoadUint64)
- sync.Condsync.WaitGroup 内部也依赖这些机制
何时需要
- 标志位(ready、shutdown)+ 条件等待
- 单例模式 + 惰性初始化
- 跨 goroutine 的状态广播/通知
5. 惰性初始化(Lazy Initialization)
场景
资源昂贵(数据库连接、HTTP 客户端、缓存、全局配置)不想在启动时立刻创建,而是在首次使用时才创建。
典型实现(带互斥与原子)
方案 A:Mutex + 双重检查
var (
once sync.Once // 最简单、最安全
config *Config
)
func GetConfig() *Config {
if config == nil {
once.Do(func() {
// 耗时加载逻辑
config = newConfig()
})
}
return config
}方案 B:sync.Once 替代手动 Mutex
sync.Once 保证函数体只执行一次,且线程安全,推荐用于“全局单点初始化”。
方案 C:指针 + atomic
适合需要区分“已初始化指针”与“nil 指针”的场景(较少用)。
何时使用
- 全局 HTTP Client、DB 连接池、Redis 客户端
- 配置文件首次加载
- 大型对象(统计器、分析器、规则引擎)首次挂载
注意事项
- 优先使用 sync.Once,避免手动双重检查锁逻辑
- 初始化过程中避免 panic(否则 once 状态不确定)
- 若初始化失败(如连接拒绝),考虑记录日志 + 回退策略
6. 竞争条件检测(Race Detector)
工具
go test -race 或 go run -race main.go
原理
Go runtime 插入读写检查点,检测是否有未加同步的共享数据访问。
何时必须使用
- 新模块开发阶段
- 发布前回归测试
- 重构并发逻辑
- 第三方库混合使用可能引入竞争
流程建议
- CI/CD 中强制 go test -race
- PR 审核时看 race 报告
- 对于关键服务,建议开启 runtime.MemProfile + 压力测试辅助定位
开发中“什么时候用”的综合决策
实践建议
1. 优先用 channel + worker pool:尽量不共享状态。
2. 共享变量能少则少:用结构体封装 + 单实例 + 外部引用,而非大量全局变量。
3. 初始化路径要稳定:用 sync.Once 包装耗时初始化。
4. 锁范围最小化:锁内只做必要操作,IO/网络在锁外。
5. CI 强制 race 检测:防止上线后出现偶现问题。
6. 文档化并发边界:在 README 或内部文档中说明哪些变量是“并发安全”的,哪些不是。