← Debugging Guides

阅读中文版本

Go — go vet copylocks: Mutex Copied by Value

Understand the copylocks vet warning when structs containing sync.Mutex are copied, see why it corrupts locking, and pass pointers or embed the mutex explicitly.

intermediatedebugginggovetconcurrency

Symptoms

  • go vet reports: assignment copies lock value (struct containing sync.Mutex) or main passes lock by value
  • Mutexes appear to be held by nobody yet Unlock panics, or mutual exclusion silently fails under concurrency
  • The problem appears after adding a mutex field to a struct that is already passed by value or returned by value

Possible causes

  • A struct with an embedded or named sync.Mutex field is assigned, passed by value, or returned by value
  • A method with a value receiver on a struct containing a mutex copies the entire struct including the lock
  • A map or slice of lock-structs is populated by copying values in
  • A range loop variable copies the lock-struct, and methods called on it lock the copy

Diagnosis steps

Run vet and read the line

go vet ./... 2>&1 | head -10

Interpretation

The vet message names the copy site — assignment, function argument, or value receiver — which is the exact fix location.

Locate all value receivers

grep -Rn "func (\w* [A-Z]\w*) " src/ | head -20

Interpretation

Value receivers on structs that contain (or later gain) a mutex are latent copylocks violations; pointer receivers never copy.

Verify with the race detector

go test -race ./counter/ -count=20

Interpretation

Locking a copied mutex provides zero mutual exclusion, so -race often demonstrates the data race the copy introduced.

Root cause

A sync.Mutex must not be copied after first use — its internal state encodes whether it is locked and who holds it. Copying the containing struct copies the lock state into a fresh mutex, so two goroutines can each lock their own copy and enter the critical section simultaneously, and Unlock can fire on a mutex held by a different copy.

Fix

Use pointer receivers on structs containing mutexes, pass and store pointers to those structs, and run go vet in CI so the class is caught before merge. When embedding, remember the mutex travels with the struct value in every copy.

Risk note

Changing receivers to pointers alters interface satisfaction and nil-usage semantics. Review callers for nil receivers as part of the change.

Verify the fix

After converting to pointer semantics, run vet clean, the race-enabled suite repeated, and a concurrency stress test asserting mutual exclusion actually holds.

Symptom walkthrough

The tooling tells you before production does:

$ go vet ./...
./src/counter.go:31:9: assignment copies lock value
to c2: counter.Counter contains sync.Mutex

The runtime symptoms are stranger than the warning: mutual exclusion silently stops working (two goroutines enter the “protected” section), or Unlock panics on a mutex nobody appears to hold. Both follow from one fact — the mutex state was duplicated:

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

The classic trap is adding a mutex to a struct that was already passed by value everywhere. The compile still succeeds; vet is the guard that fires.

Fix in practice

Pointer receivers for any struct holding a lock:

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++
}

Pass and store pointers:

func aggregate(counters []*Counter) int   // not []Counter

Range copies are sneaky — bind by pointer or index:

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()
}

And keep the guard armed in CI: go vet ./… on every pull request, because the class reappears the moment someone adds a lock field to a value-passed struct.