Preventing accidental struct copies in Go

Redowan Delowar April 21, 2025
Source

By default, Go copies values when you pass them around. But sometimes, that can be undesirable. For example, if you accidentally copy a mutex and multiple goroutines work on separate instances of the lock, they won't be properly synchronized. In those cases, passing a pointer to the lock avoids the copy and works as expected.

Take this example: passing a sync.WaitGroup by value will break things in subtle ways:

sync.WaitGroup lets you wait for multiple goroutines to finish some work. Under the hood, it's a struct with methods like Add, Done, and Wait to sync concurrently running goroutines.

That snippet compiles fine but leads to buggy behavior because we're copying the lock instead of referencing it in the f function.

Luckily, go vet catches it. If you run vet on that code, you'll get a warning like this:

This means we're passing wg by value when we should be passing a reference. Here's the fix:

Since this kind of incorrect copy doesn't throw a compile-time error, if you skip go vet, you might never catch it. Another reason to always vet your code.

I was curious how the Go toolchain enforces this. The clue is in the vet warning:

So the sync.noCopy struct inside sync.WaitGroup is doing something to alert go vet when you pass it by value.

Looking at the implementation of sync.WaitGroup, you'll see:

Then I traced the definition of noCopy in sync/cond.go:

Just having those no-op Lock and Unlock methods on noCopy is enough. This implements the Locker interface. Then if you put that struct inside another one, go vet will flag cases where you try to copy the outer struct.

Also, note the comment: don't embed noCopy. Include it explicitly. Embedding would expose Lock and Unlock on the outer struct, which you probably don't want.

The Go toolchain enforces this with the copylock checker. It's part of go vet. You can exclusively invoke it with go vet -copylocks ./.... It looks for value copies of any struct that nests a struct with Lock and Unlock methods. It doesn't matter what those methods do, just having them is enough.

When vet runs, it walks the AST and applies the checker on assignments, function calls, return values, struct literals, range loops, channel sends, basically anywhere values can get copied. If it sees you copying a struct with noCopy, it yells.

Interestingly, if you define noCopy as anything other than a struct and implement the Locker interface, vet ignores that. I tested this on Go 1.24:

This doesn't trigger vet. It only works when noCopy is a struct. The reason is that vet takes a shortcut in the copylock checker when deciding whether to trigger the warning. Currently, it explicitly looks for a struct that satisfies the Locker interface and ignores any other type even if it implements the interface.

You'll see this in other parts of the sync package too. sync.Mutex uses the same trick:

Same with sync.Once:

Here's a complete example of abusing -copylocks to prevent copying our own struct:

Running go vet on this gives:


Someone on Reddit asked me what actually triggers the copylock checker in go vet - is it the struct's literal name noCopy or the fact that it implements the Locker interface?

The name noCopy isn't special. You can call it whatever you want. As long as it implements the Locker interface, go vet will complain if the surrounding struct gets copied. See this Go Playground snippet.

Discussion in the ATmosphere

Loading comments...