sync.mutex必须声明为值类型字段、用指针接收者方法调用lock/unlock,禁止复制或指针初始化,必须成对使用且锁粒度要小。

Go 语言没有叫 “互斥函数” 的东西,加锁靠的是 sync.Mutex 类型的实例,不是函数;所谓“同步”,本质是用 Lock() 和 Unlock() 控制临界区——这点不搞清,代码迟早出竞态。
sync.Mutex 必须是值类型,不能是指针类型传参
常见错误是把 sync.Mutex 声明为指针(比如 *sync.Mutex),然后在方法里调用 mu.Lock() ——表面能跑,但实际锁失效。因为 sync.Mutex 是不可拷贝类型,且其内部状态靠地址绑定。
- 正确做法:字段声明为
mu sync.Mutex(值类型),方法接收者用指针(func (c *Counter) Inc() { c.mu.Lock() ... }) - 错误写法:
mu *sync.Mutex初始化为new(sync.Mutex),会导致每次方法调用都操作一个新副本,锁形同虚设 - 编译器不会报错,但
go run -race会立刻暴露 data race
Lock/Unlock 必须成对出现,defer 最安全
忘记 Unlock() 是最隐蔽的死锁来源;手动配对容易漏,尤其函数有多个 return 路径时。
- 强制用
defer mu.Unlock()放在Lock()后紧邻行,无论函数怎么 return 或 panic 都能释放 - 不要写
if err != nil { mu.Unlock(); return }这类提前释放逻辑——它破坏了临界区边界,可能让其他 goroutine 在非预期时机进入 - 锁的粒度要小:只包住真正读写共享变量的几行,别把网络调用、日志打印、循环全塞进去
不要在 Lock 持有时调用可能阻塞或重入的函数
sync.Mutex 不支持重入(即同一线程/协程重复 Lock 会死锁),且持有锁期间若调用外部函数(如 http.Get、time.Sleep、甚至某些 channel 操作),会拖慢所有等待该锁的 goroutine。
- 避免在
Lock()和Unlock()之间做任何 I/O、系统调用、或调用可能间接触发调度的操作 - 如果必须处理耗时逻辑,先在锁外复制所需数据,再解锁,最后处理
- 注意:
fmt.Println在高并发下可能隐式锁 stdout,虽不影响你的mu,但会放大锁等待感知——调试时可临时去掉
sync.Mutex 不是万能的,别和 channel 混用解决同一问题
看到“多个 goroutine 协作”,第一反应不该是加锁,而是问:这事能不能用 channel 拆解?比如任务分发、结果收集、状态通知等场景,channel 往往比 Mutex 更清晰、更少出错。
- Mutex 适合保护「就地修改」的共享状态(如计数器、缓存 map、配置结构体字段)
- Channel 适合「传递所有权」或「控制流协调」(如 worker pool、信号广播、背压控制)
- 两者混用(比如一边用 channel 发送数据,一边用 mutex 保护发送前的校验逻辑)没问题,但别为了“看起来像并发编程”而硬套 Mutex
最容易被忽略的一点:Mutex 的零值是可用的——你不需要显式初始化 var mu sync.Mutex,直接声明就能用。但很多人习惯写 mu: sync.Mutex{} 或 new(sync.Mutex),后者反而埋雷。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











