How Go detects struct copies with sync.noCopy — Phuong Le
If you have read the source code of the sync package, you may have noticed that several structs contain an unusual field of type noCopy, such as sync.Mutex, sync.Once, and sync.Map:<br>go<br>type Mutex struct {<br>_ noCopy<br>...
type Once struct {<br>_ noCopy<br>...
type Map struct {<br>_ noCopy<br>...
noCopy is a special marker for types that must not be copied after their first use. But the marker itself is only an empty struct with two empty methods:<br>go<br>type noCopy struct{}
func (*noCopy) Lock() {}<br>func (*noCopy) Unlock() {}
Despite the method names, there is no lock and nothing gets unlocked. But nothing here stops us from copying the value. This post explains what can break after a copy, why noCopy needs these two methods, and how to add the same marker to your own types.<br>1. What noCopy does and does not do<br>The noCopy marker does not add any special rule to the Go compiler. You can still copy a sync.Map after it has been used:<br>go<br>var a sync.Map<br>a.Store("k", 1)
b := a // copying a sync.Map
The assignment copies all the fields from a into b, exactly as it would for any other struct value. The code still passes go build because the compiler gives no special meaning to the name noCopy or to its Lock and Unlock methods.<br>It turns out that the warning comes from a separate tool called go vet. This static-analysis command is included with Go and reports suspicious code that the compiler still accepts. When go vet checks the same assignment, it reports:<br>assignment copies lock value to b: sync.Map contains sync.noCopy
The phrase copies lock value comes from the purpose of the copylocks checker in go vet. It was created to report copies of values that contain a lock, such as sync.Mutex, because copying a lock after it has been used also copies its internal state.<br>The behavior is easy to reproduce with a regular struct that contains sync.Mutex:<br>go<br>type Counter struct {<br>mu sync.Mutex<br>value int
func main() {<br>var a Counter<br>b := a<br>_ = b.value
$ go vet ./...<br>main.go:12:7: assignment copies lock value to b: example.com/nocopy-repro.Counter contains sync.Mutex
Counter contains an actual mutex, so copying the outer struct also copies the mutex state.<br>2. How go vet and noCopy work<br>So go vet produces this warning through its copylocks checker and this checker does not search for a field named noCopy. It uses the following rule while inspecting the copied type and its fields:<br>go<br>if types.Implements(types.NewPointer(typ), lockerType) &&<br>!types.Implements(typ, lockerType) {<br>return []string{typ.String()}
The checker starts with the type being copied (e.g., Counter) and asks whether its pointer implements sync.Locker while its value does not. If the type is a struct and the answer is no, the checker applies the same rule to the type of every field, including fields inside nested structs. For Counter, this search finds sync.Mutex in the mu field.<br>COPYLOCKS RULE*T implements sync.LockerT does not implement sync.LockerCountersync.Onceno matchcheck field muno matchcheck field _ noCopysync.MutexRULE MATCHESnoCopyRULE MATCHESThe checker applies the same rule to the outer type and its field types.<br>For sync.Once, it finds noCopy in the _ noCopy field. This recursive search is why the warning can report that an outer struct contains a lock or noCopy.<br>That is why the noCopy marker’s methods are named Lock and Unlock. As mentioned, the rule above checks whether a type implements sync.Locker. This interface contains exactly two methods:<br>go<br>type Locker interface {<br>Lock()<br>Unlock()
type noCopy struct{}
func (*noCopy) Lock() {}<br>func (*noCopy) Unlock() {}
These names make the pointer *noCopy implement sync.Locker, while the value noCopy does not.<br>But you may notice that some structs in the sync package already contained a sync.Mutex, such as sync.Map:<br>go<br>type Map struct {<br>mu Mutex<br>...
If go vet could already find the mutex, why does sync.Map also need _ noCopy?<br>sync.Map did not need _ noCopy merely to trigger a warning. copylocks could already reach its mu Mutex field. The explicit markers in sync.Map, sync.Once, and sync.Mutex have three purposes.<br>First, _ noCopy gives each struct an explicit marker, so copylocks does not depend on how that struct is implemented. The checker can find _ noCopy directly instead of finding an internal mutex or relying on the outer type’s Lock and Unlock methods.<br>Second, the warning describes the copying restriction more clearly. The old warning for sync.Map named the mutex found inside it:<br>assignment copies lock value to b: sync.Map contains sync.Mutex
The current warning names noCopy instead. The name directly indicates that the outer type must not be copied:<br>assignment copies lock value to b: sync.Map contains sync.noCopy
Third, the marker fixes a false negative involving sync.Mutex. A false negative means the checker should report a copy but does not. The checker recognized sync.Mutex through its Lock and Unlock methods, but a new named type does...