How Go detects struct copies with sync.noCopy
Understanding how Go detects struct copies via sync.noCopy
If you've spent time diving into the Go standard library's sync package, you might have encountered a peculiar field named _ noCopy inside types like sync.Mutex, sync.Once, or sync.Map.
type Mutex struct {
_ noCopy
// ... other fields
}
The noCopy type serves as a marker to signal that a struct should not be copied after its initial use. Surprisingly, the definition of this marker is entirely hollow:
type noCopy struct{}
func (*noCopy) Lock() {}
func (*noCopy) Unlock() {}
Despite the method names, these functions perform no actual locking logic. They exist solely for a specific purpose. This article explores why copying certain structs is dangerous, how noCopy triggers warnings, and how you can implement this pattern yourself.
What noCopy Does (and Doesn't) Do
It is a common misconception that noCopy is a compiler directive. In reality, it provides no special instructions to the Go compiler.
For example, the following code will compile and run without any issues:
var a sync.Map
a.Store("k", 1)
b := a // This copies the sync.Map
The assignment b := a simply copies all fields from a to b, just as it would for any other struct. The go build command remains silent because the compiler doesn't care about the name noCopy or its methods.
The warning actually originates from go vet, a static analysis tool bundled with Go that identifies "suspicious" code that is technically legal but likely incorrect.
go vet output:
assignment copies lock value to b: sync.Map contains sync.noCopy
This warning is generated by the copylocks checker. Its primary goal is to prevent the copying of values that contain locks (like sync.Mutex), because copying a lock also copies its internal state (e.g., whether it is currently locked), leading to unpredictable behavior.
Example: Reproducing the Warning
You can trigger this warning with a simple custom struct:
type Counter struct {
mu sync.Mutex
value int
}
func main() {
var a Counter
b := a // Warning triggered here
}
Result: main.go:12:7: assignment copies lock value to b: example.com/nocopy-repro.Counter contains sync.Mutex
The Mechanics of go vet and noCopy
The copylocks checker doesn't specifically look for a field named "noCopy." Instead, it follows a logical rule based on interface implementation.
The Logic Rule
The checker evaluates whether a type satisfies a specific condition regarding the sync.Locker interface.
| Type | Value () implements Locker? | Pointer () implements Locker? | Match? |
|---|---|---|---|
int | No | No | ❌ |
noCopy | No | Yes | ✅ |
sync.Mutex | No | Yes | ✅ |
Recursive Inspection Process
The checker starts at the top-level type being copied. If the top-level type doesn't match the rule, it recursively inspects every field within the struct.
This is why the noCopy methods are named Lock and Unlock. They perfectly mirror the sync.Locker interface:
type Locker interface {
Lock()
Unlock()
}
By defining these methods on the pointer *noCopy rather than the value noCopy, the marker satisfies the copylocks rule.
Evolution and Purpose of the Marker
This pattern was introduced to the standard library in 2016 by Aliaksandr Valialkin (CTO of VictoriaMetrics), following a proposal by Russ Cox. The Unlock method was added in 2018 when the checker shifted to using the sync.Locker interface.
You might wonder: If sync.Map already contains a sync.Mutex, why add _ noCopy?
There are three primary reasons for this explicit marker:
- Implementation Decoupling: It ensures
go vetdoesn't rely on the internal implementation details of the struct. Even if the internal mutex is renamed or replaced, the_ noCopymarker remains. - Improved Clarity: The warning message becomes more explicit. Instead of saying it found a
sync.Mutexinside async.Map, it explicitly states the type containssync.noCopy, signaling that the entire outer struct is non-copyable. - Eliminating False Negatives: A "false negative" occurs when the tool fails to report an error it should have. Consider a named type:
type LocalMutex sync.Mutex
Previously, copylocks might miss a copy of LocalMutex because the new type doesn't inherit the methods of sync.Mutex. However, because LocalMutex still contains the _ noCopy field in its underlying structure, the recursive search will find it.
The Case of sync.RWMutex
Interestingly, sync.RWMutex is the only non-copyable exported struct in the sync package that lacks a noCopy field. Yet, go vet still catches it. Why?
- Direct Copy:
*RWMuteximplementssync.Locker, whileRWMutexdoes not. - Named Type Copy: If you create
type LocalRWMutex sync.RWMutex, the checker finds the internalw Mutexfield:
type RWMutex struct {
w Mutex // <--- Recursive search finds this!
writerSem uint32
// ...
}
Summary Checklist for go vet
To determine if a type will trigger a "copies lock value" warning, go vet essentially runs this checklist:
- Does the value type implement
Lock()andUnlock()? (If yes, it's likely intended to be copied; skip). - Does the pointer type implement
Lock()andUnlock()? (If yes, Warning!). - If neither, is a struct?
- If yes, repeat the check for every field inside the struct.