← 中文资源

简体中文版本

Go — go vet copylocks:锁按值被复制

理解含 sync.Mutex 的结构体被复制时的 copylocks 警告,弄清它为何破坏互斥,并改为传指针或显式嵌入。

阅读英文原文

症状

  工具链会在生产之前告诉你:go vet 报出 assignment copies lock value to c2: counter.Counter contains sync.Mutex。运行期症状比警告更诡异:互斥静默失效(两个 goroutine 同时进入“受保护”段),或 Unlock 在一个看似无人持有的锁上 panic。两者都源于同一事实——锁的状态被复制了:c2 := c1 把使用中的 mutex 复制进新值,随后 c2.mu.Lock() 锁的是副本,c1.mu.Lock() 也照样成功,因为它们是两把不同的锁。经典陷阱是给一个早已按值传递的结构体新加 mutex 字段:编译照旧通过,vet 才是会开火的卫兵。

诊断思路

  先跑 go vet 读出复制点(赋值、函数实参或值接收者),那就是修复位置;再全局排查值接收者——含锁或将来可能含锁的结构体上的值接收者都是潜伏违规,指针接收者永不复制;最后用 go test -race -count=20 重复跑,锁副本提供的互斥为零,竞态检测器常能直接演示复制引入的数据竞态。range 循环变量对锁结构体做值拷贝、再对其调用方法,是最隐蔽的复制形态。

根因

  sync.Mutex 在首次使用后不可复制——其内部状态编码了是否加锁与持有者。复制包含它的结构体就把锁状态复制进一个新锁,两个 goroutine 各锁各的副本同时进入临界区,Unlock 也可能解锁一把由另一个副本持有的锁。

修复与验证

  含锁结构体一律指针接收者;传参与存储都用指针([]*Counter 而不是 []Counter);range 循环按下标操作元素本身(regs[i].Inc())而不是迭代副本;CI 里把 go vet ./… 设为合并门禁,因为这个类别会在某人给值传递结构体加锁字段的那一刻复活。改指针接收者会牵动接口满足与 nil 语义,需要顺带审查调用方的 nil 接收者用法。验证时 vet 清零、竞态套件重复运行零报告、并发压力测试断言互斥真实成立——三关全过才算修复。

代码示例

$ go vet ./...
./src/counter.go:31:9: assignment copies lock value
to c2: counter.Counter contains sync.Mutex
c2 := c1          // copies the mutex mid-use: two locks, zero exclusion
c2.mu.Lock()      // locks the COPY, not the original
c1.mu.Lock()      // also succeeds: different mutex entirely
type Counter struct {
    mu sync.Mutex
    n  int
}

func (c *Counter) Inc() {   // pointer receiver: no copy, real exclusion
    c.mu.Lock()
    defer c.mu.Unlock()
    c.n++
}
func aggregate(counters []*Counter) int   // not []Counter
for _, c := range regs {   // c is a COPY if regs is []Counter
    c.Inc()                 // locks the copy
}
for i := range regs {       // fix: operate on the element itself
    regs[i].Inc()
}