{
  "$type": "site.standard.document",
  "canonicalUrl": "https://rednafi.com/go/prevent-struct-copies/",
  "description": "Prevent dangerous struct copies with noCopy sentinel and go vet's copylock checker. Protect mutexes and sync primitives from value copies.",
  "path": "/go/prevent-struct-copies/",
  "publishedAt": "2025-04-21T00:00:00.000Z",
  "site": "at://did:plc:fgtm2c26vfcj74rfmeggbyqj/site.standard.publication/3mnl6f7ob462z",
  "tags": [
    "Go",
    "TIL",
    "Concurrency"
  ],
  "textContent": "By default, Go copies values when you pass them around. But sometimes, that can be\nundesirable. For example, if you accidentally copy a mutex and multiple goroutines work on\nseparate instances of the lock, they won't be properly synchronized. In those cases, passing\na pointer to the lock avoids the copy and works as expected.\n\nTake this example: passing a sync.WaitGroup by value will break things in subtle ways:\n\nsync.WaitGroup lets you wait for multiple goroutines to finish some work. Under the hood,\nit's a struct with methods like Add, Done, and Wait to sync concurrently running\ngoroutines.\n\nThat snippet compiles fine but leads to buggy behavior because we're copying the lock\ninstead of referencing it in the f function.\n\nLuckily, go vet catches it. If you run vet on that code, you'll get a warning like this:\n\nThis means we're passing wg by value when we should be passing a reference. Here's the\nfix:\n\nSince this kind of incorrect copy doesn't throw a compile-time error, if you skip go vet,\nyou might never catch it. Another reason to always vet your code.\n\nI was curious how the Go toolchain enforces this. The clue is in the vet warning:\n\nSo the sync.noCopy struct inside sync.WaitGroup is doing something to alert go vet\nwhen you pass it by value.\n\nLooking at the implementation of [sync.WaitGroup], you'll see:\n\nThen I traced the definition of noCopy in [sync/cond.go]:\n\nJust having those no-op Lock and Unlock methods on noCopy is enough. This implements\nthe [Locker] interface. Then if you put that struct inside another one, go vet will flag\ncases where you try to copy the outer struct.\n\nAlso, note the comment: don't _embed_ noCopy. Include it explicitly. Embedding would\nexpose Lock and Unlock on the outer struct, which you probably don't want.\n\nThe Go toolchain enforces this with the [copylock checker]. It's part of go vet. You can\nexclusively invoke it with go vet -copylocks ./.... It looks for value copies of any\nstruct that nests a struct with Lock and Unlock methods. It doesn't matter what those\nmethods do, just having them is enough.\n\nWhen vet runs, it walks the AST and applies the checker on assignments, function calls,\nreturn values, struct literals, range loops, channel sends, basically anywhere values can\nget copied. If it sees you copying a struct with noCopy, it yells.\n\nInterestingly, if you define noCopy as anything other than a struct and implement the\nLocker interface, vet ignores that. I tested this on Go 1.24:\n\nThis doesn't trigger vet. It only works when noCopy is a struct. The reason is that vet\ntakes a [shortcut in the copylock checker] when deciding whether to trigger the warning.\nCurrently, it explicitly looks for a struct that satisfies the Locker interface and\nignores any other type even if it implements the interface.\n\nYou'll see this in other parts of the sync package too. sync.Mutex uses the same trick:\n\nSame with sync.Once:\n\nHere's a complete example of abusing -copylocks to prevent copying our own struct:\n\nRunning go vet on this gives:\n\n---\n\nSomeone on Reddit asked me what actually triggers the copylock checker in go vet - is it\nthe struct's literal name noCopy or the fact that it implements the Locker interface?\n\nThe name noCopy isn't special. You can call it whatever you want. As long as it implements\nthe Locker interface, go vet will complain if the surrounding struct gets copied. See\nthis Go Playground [snippet].\n\n\n\n\n[sync.WaitGroup]:\n    https://cs.opensource.google/go/go/+/refs/tags/go1.24.2:src/sync/waitgroup.go;l=25-30\n\n\n[sync/cond.go]:\n    https://cs.opensource.google/go/go/+/refs/tags/go1.24.2:src/sync/cond.go;l=111-122\n\n[locker]:\n    https://github.com/golang/go/blob/336626bac4c62b617127d41dccae17eed0350b0f/src/sync/mutex.go#L37\n\n[copylock checker]:\n    https://cs.opensource.google/go/x/tools/+/master:go/analysis/passes/copylock/copylock.go;l=39;drc=bacd4ba3666bbac3f6d08bede00fdcb2f5cbaacf\n\n[shortcut in the copylock checker]:\n    https://cs.opensource.google/go/x/tools/+/refs/tags/v0.32.0:go/analysis/passes/copylock/copylock.go;l=338\n\n\n[snippet]:\n    https://go.dev/play/p/M-vR6nOn00j",
  "title": "Preventing accidental struct copies in Go"
}