必须用 sync.Mutex 保护所有对共享变量的读写操作,只要多个 goroutine 同时访问且含写操作,不加锁必然触发 data race;结构体需嵌入 sync.Mutex 并使用指针接收者,Lock() 后必须紧跟 defer Unlock(),读操作同样需加锁,锁内禁止耗时操作。

必须用 sync.Mutex 保护所有对共享变量的读写操作,只要多个 goroutine 同时访问且含写操作,不加锁就必然触发 data race —— go run -race 会直接报错,不是“可能出问题”,是运行即崩。
结构体嵌入 sync.Mutex 并用指针接收者调用
锁必须和被保护的数据绑定在同一实例上,否则复制值、传值接收者都会让锁失效。
- 错误:用值接收者
func (c Counter) Inc()→ 每次调用都复制整个结构体,包括mu sync.Mutex,两把锁互不干扰 - 错误:局部声明
var mu sync.Mutex在函数内 → 每次调用新建一把锁,完全没用 - 正确:嵌入结构体字段 + 指针接收者 + 所有方法统一通过
*Counter访问
示例:
type Counter struct {
mu sync.Mutex
value int
}
func (c *Counter) Inc() {
c.mu.Lock()
defer c.mu.Unlock()
c.value++
}
func (c *Counter) Get() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.value
}
Lock() 后必须紧跟 defer Unlock()
不是“放在函数末尾”,而是紧接在 Lock() 后面写,覆盖所有退出路径(return、panic、中间 error 判断)。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
立即学习“go语言免费学习笔记(深入)”;
- 错误写法:
mu.Lock()放开头,defer mu.Unlock()放函数最后 → 中间if err != nil { return }会导致锁永不释放 - 危险写法:在锁内调用可能 panic 的函数(如
json.Marshal、未判空解引用)→defer不触发,锁永久卡住 - 安全底线:
mu.Lock()和defer mu.Unlock()必须成对出现在同一作用域开头,中间不插任何分支逻辑
读操作也必须加锁,不能只锁写
只要存在并发写,所有读操作同样需要锁——否则读到的是写操作中途的“撕裂态”(比如 map 正在 rehash、int64 写一半)。
- 典型误区:“我只在写的地方加锁,读是安全的” → 错。Go 的
map并发读写会直接fatal error: concurrent map reads and writes - 即使只是读一个
int字段,只要同时有 goroutine 在执行c.value++,就必须锁住整个读取路径 -
sync.RWMutex是优化手段,不是替代方案;读多写少才考虑它,且仍要确保所有写操作走Lock(),所有读走RLock()
锁内禁止做任何耗时操作
锁本身不报错,但把网络请求、文件 IO、JSON 序列化、长循环塞进临界区,会让其他 goroutine 长时间排队,吞吐暴跌——这不是 bug,是设计缺陷。
- 正确做法:先算好要写的数据(比如拼好字符串、构造好 struct),再加锁更新共享状态
- 常见翻车点:
mu.Lock(); defer mu.Unlock(); resp, _ := http.Get(...); c.cache[key] = resp.Body→ HTTP 请求在锁内,锁持有数秒 - 更隐蔽的问题:锁内调用未加锁的第三方库方法,而该方法内部又修改了你没意识到的共享状态
真正难的不是“怎么加锁”,而是判断哪些变量是共享的、哪些访问路径被漏掉了——go run -race 是唯一可信的验证方式,别靠肉眼猜。

















