logo
Kurashizu Blogwhere ideas flow
  • Home
  • News
  • Blog
  • About
  • Home
  • News
  • Blog
  • About

© 2026 Kurashizu. All rights reserved.

Admin·New Post·Service Status
Powered by Cloudflare·31.10.26
← Back to news

How Go detects struct copies with sync.noCopy

Original article
func25.dev|55 points|51 comments|by func25|Aug 13, 2026

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 TTT satisfies a specific condition regarding the sync.Locker interface.

Rule: (∗T implements sync.Locker)∧(T does not implement sync.Locker)\text{Rule: } (*T \text{ implements } \text{sync.Locker}) \land (T \text{ does not implement } \text{sync.Locker})Rule: (∗T implements sync.Locker)∧(T does not implement sync.Locker)

TypeValue (TTT) implements Locker?Pointer (∗T*T∗T) implements Locker?Match?
intNoNo❌
noCopyNoYes✅
sync.MutexNoYes✅

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:

  1. Implementation Decoupling: It ensures go vet doesn't rely on the internal implementation details of the struct. Even if the internal mutex is renamed or replaced, the _ noCopy marker remains.
  2. Improved Clarity: The warning message becomes more explicit. Instead of saying it found a sync.Mutex inside a sync.Map, it explicitly states the type contains sync.noCopy, signaling that the entire outer struct is non-copyable.
  3. 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: *RWMutex implements sync.Locker, while RWMutex does not.
  • Named Type Copy: If you create type LocalRWMutex sync.RWMutex, the checker finds the internal w Mutex field:
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 TTT implement Lock() and Unlock()? (If yes, it's likely intended to be copied; skip).
  • Does the pointer type ∗T*T∗T implement Lock() and Unlock()? (If yes, Warning!).
  • If neither, is TTT a struct?
    • If yes, repeat the check for every field inside the struct.