sync and atomic in Go: two pieces of advice that fall apart under measurement
The interview climbs a ladder: what is wrong with a plain increment — how atomic differs from a mutex — when to reach for an RWMutex — what Once does — why go vet catches a copied mutex. Measured: without contention a mutex is twice as dear as an atomic, but as contenders grow it becomes 9.6 times dearer while the atomic barely moves; and on a short section an RWMutex loses to a plain Mutex.
Full technical treatment
TL;DR
A plain n++ from several goroutines loses updates, because it is not one
action but three: read, add, write. Two goroutines manage to read the same
number, both add one, both write — and one of the additions disappears.
Synchronisation exists not for speed but to stop that happening.
Hence the main consequence: the loss does not happen every time — and that is worse than if it always did. The code "works" until the load changes: in one run, eight goroutines of 10,000 increments produced exactly 80,000, meaning nothing was lost at all. And hence, too, the division of labour worth stating out loud: a mutex protects the integrity of a region of code — any number of fields and any logic between them; an atomic operation fits where the state is bounded and changes in one indivisible action.
Beyond that come the numbers, and they overturn two common pieces of
advice. The guarantees, meanwhile, come not from the word "lock" but from the
memory model: happens-before — which is why "I put a mutex on it, so it is safe"
holds only if all access goes through it. Without contention a mutex is
twice as dear as an atomic — 13.35 ns against 6.49; but as contenders grow from
2 to 16 the mutex becomes 9.6 times dearer while the atomic becomes 1.0:
the mutex parks the loser, the atomic operation retries. "Use an RWMutex for
reads" is wrong on a short section: with an empty section RLock loses to
Lock (98.78 against 24.01 ns), and the gain appears only on a long one —
671.01 against 1463.55. sync.Once is free after the first call — 1.87 ns,
within noise of doing nothing at all. And a copy of a struct holding a mutex is
a different mutex; go vet catches it with the copylocks check, which already
runs inside go test. The numbers were taken on one machine and one version of
Go: what is stable is the directions and the ratios, not the nanoseconds
themselves.
- several goroutines can work with the same data at the same time;
- a variable lives in shared memory: it is visible to more than the goroutine that created it;
n++looks like a single action in the code — and half the mistakes in this topic rest on that impression.
- what happens-before is and what the Go memory model says about it;
Mutex,RWMutex,atomic,Once,WaitGroup;- the
copylockscheck ingo vet, and cache lines.
What is really being asked
The ladder is almost always this one:
- "What is wrong with
n++across goroutines?" — the warm-up. - "How does atomic differ from a mutex?" — here people start with "it is faster", which is half an answer.
- "Which is faster, and by how much?" — a trap: the right answer starts with a question back, "at what contention?"
- "When do you use an RWMutex?" — another trap: "when there are many reads" is wrong.
- "Why
sync.Onceif a flag would do?" — about happens-before, not speed. - "Why must a mutex not be copied?" — and what catches it.
The lesson climbs that ladder. Its spine is one sentence: synchronisation is about guarantees rather than speed; and speed depends on contention rather than on the primitive.
Base: a data race — and why n++ is not one action
The situation the whole topic grows from looks harmless: two goroutines work with the same variable, and at least one of them changes it. That is what a data race is.
It seems nothing can go wrong: an increment is one line. But one line in the
source says nothing about indivisibility. n++ is three actions:
- read the current value from memory;
- add one to it;
- write the result back.
Between any two of them another goroutine manages to do exactly the same. Then both read the same number, both add one, both write the result — and instead of two additions there is one. An update has disappeared, and nobody reported it: no error, no panic, not a line in the log.
That is how it looks in a run. Eight goroutines, 10,000 increments each, of a plain variable:
n := 0
// eight goroutines: for i := 0; i < 10000; i++ { n++ }A run of bench/gosync/practice.go reports that the total is less than
80,000. The same counter done atomically gives exactly 80,000.
A caveat about this measurement, and it matters more than the measurement.
In the script the read and the write are separated explicitly, with a
runtime.Gosched() between them. That was done not for clarity but because the
first draft wrote a plain n++ and one of the runs produced exactly 80,000:
no loss occurred. That does not disprove the race — it is the race's defining
property: it does not show up every time. Which is exactly why such bugs are
not found by tests: the code "works" until the load, the Go version or the core
count changes.
And here is the question this lesson is about: what has to be added to the code so that one goroutine's three actions do not interleave with another's? In general terms: a place where goroutines agree on an order — and Go has several of those. They differ not in speed but in how big a piece of work each of them can make indivisible.
That is already enough to answer the basic interview question, "what is wrong
with n++ across goroutines". What follows is what actually closes that gap:
first the mutex, then the read-write lock, atomic operations, run-once
initialisation and, at the very end, the price paid not to the scheduler but to
the hardware.
Mechanism 1: the guarantee comes from happens-before, not from the lock
The place to start is not the tool but where the word "safe" comes from at all. Not from the word "lock" but from the memory model:
For any sync.Mutex or sync.RWMutex variable l and n < m, call n of l.Unlock()
is synchronized before call m of l.Lock() returns.
Hence a practical consequence worth stating out loud: a mutex protects an invariant, not a variable. The difference is not a matter of words.
"Protects a variable" suggests it is enough to take the lock around every access to a field. That is not enough: if a struct has two fields that must stay consistent — a length and its contents, a balance and its ledger — then the lock must cover the whole operation that moves them from one consistent state to another.
// protects a variable — and does not work
mu.Lock(); n := len(items); mu.Unlock()
mu.Lock(); v := items[n-1]; mu.Unlock() // items is already different by now
// protects an invariant
mu.Lock()
v := items[len(items)-1]
mu.Unlock()And the other half of the same point: if even one goroutine reads the field
outside the lock, there is no guarantee at all — the compiler and the
processor are free to reorder anything, and -race will find it.
Which edge each primitive creates
The rest of the lesson covers five different tools, and they all do one thing: they create a happens-before edge between two points in different goroutines. They differ only in which points.
| primitive | the edge: what comes "before" | what comes "after" |
|---|---|---|
Mutex | Unlock() | the return from the next Lock() |
RWMutex | Unlock() | the return from the next RLock() |
atomic | a write | a read that saw that write |
Once | the completion of f() | the return from any Do(f) |
WaitGroup | the Done() calls | the return from Wait() |
| a channel | a send | the completion of the receive |
That table is the whole lesson in compressed form. Everything else explains how those edges differ in price and in what they can be used to protect.
Mechanism 2: atomic is not "a fast mutex"
The difference is not speed but what exactly is protected.
An atomic operation protects one cell and performs one indivisible action on it: add, swap, compare-and-swap. It covers neither two fields nor an invariant between them.
A mutex protects a region of code — any number of fields and any logic between them.
In one sentence, worth keeping in mind for the whole lesson: a mutex protects the integrity of a region of code, while atomic operations fit bounded state that is updated by a single indivisible operation.
Hence the rule for choosing, and it is not about performance: an atomic fits exactly when the protected state is one machine word. A counter, a flag, a pointer to an immutable snapshot. As soon as it becomes "read A, decide, write B", it is a mutex, and no atomicity substitutes for it.
The package talks you out of itself:
These functions require great care to be used correctly. Except for special,
low-level applications, synchronization is better done with channels or the
facilities of the sync package.
Separately about the notation: since Go 1.19 there are types —
atomic.Int64, atomic.Bool, atomic.Pointer[T]. They are preferable to the
functions (atomic.AddInt64(&x, 1)) for two reasons: a type cannot be read
non-atomically by accident, and it solves alignment on 32-bit platforms, where
an int64 needs 8-byte alignment.
Mechanism 3: "which is faster" is an incomplete question
Here is the measurement this lesson was written for.
Without contention a mutex is twice as dear as an atomic: 13.35 ns against 6.49. That is an honest number, and it is usually where the conversation ends.
But the question is about contention. One counter, varying goroutines:
| goroutines | mutex | atomic | ratio |
|---|---|---|---|
| 2 | 22.45 ns | 24.01 ns | 0.93 |
| 4 | 94.78 | 26.00 | 3.64 |
| 8 | 135.27 | 26.12 | 5.18 |
| 16 | 215.42 | 24.78 | 8.69 |
| growth | ×9.6 | ×1.0 |
Look not at individual cells — they are noisy — but at the last row. The mutex grows nearly an order of magnitude with contention, while the atomic barely grows. The reason is parking: a mutex takes the losing goroutine off the CPU and the cost of waiting enters the measurement; an atomic operation parks nobody and simply retries.
What this does not mean. It does not mean an atomic is free under load: the cell is shared by everyone, and the cache line under it travels between cores. The price is simply paid by the bus rather than by the scheduler.
And the main practical point: if sixteen goroutines fight over one cell, the primitive is no longer the issue. The cheapest fix is removing the contention itself — a counter per goroutine summed at the end, sharding by key, local accumulation. Swapping a mutex for an atomic is an optimisation within one order of magnitude; removing contention is within two.
And the boundary of these numbers is named right here: they were taken on two cores and one version of Go. What travels from here is not "9.6" or "8.7" but the cause: the mutex pays by parking the loser, the atomic operation pays by agreement between cores. A number of your own, if a decision needs one, is measured on your own load and your own hardware rather than taken from somebody else's table.
Mechanism 4: an RWMutex is not a free replacement for a Mutex
"Many reads, so use an RWMutex" sounds obvious and does not survive checking. Before looking at the numbers it is worth naming what the answer actually depends on — otherwise the table looks like a refutation of common sense.
The choice between Mutex and RWMutex is settled by three quantities, and none
of them is "how many readers there are in the code":
| quantity | what it means | favours |
|---|---|---|
| section length | how long is spent under the lock | long — RWMutex |
| read fraction | what share of entries only read | high — RWMutex |
| contention | how many goroutines want the lock at once | heavy — RWMutex |
The trap is that RLock is dearer than Lock: it counts readers and checks
whether a writer is waiting. That surcharge is always paid, while the gain from
parallel readers only appears when there is something to parallelise — that is,
when the section is long enough.
| work under the lock | RWMutex | Mutex | faster |
|---|---|---|---|
| none | 98.78 ns | 24.01 ns | Mutex |
| 10 iterations | 52.88 | 24.57 | Mutex |
| 100 iterations | 164.64 | 113.40 | Mutex |
| 1000 iterations | 671.01 | 1463.55 | RWMutex |
The reason is simple: RLock does more work than Lock — it counts readers
and checks whether a writer is waiting. On a short section that difference is
the whole operation, and parallel readers do not have time to repay it. The gain
appears only once enough time is spent under the lock.
What is stable here is the crossover itself, not where exactly it falls: the boundary depends on the machine and its core count.
Two more things worth knowing about RWMutex:
- It is not recursive. Taking
RLocktwice in one goroutine is allowed, but if aLockfrom another goroutine wedges in between, the secondRLockblocks and you have a deadlock — the documentation warns about this outright. - Writers do not starve for exactly that reason: a waiting
Lockblocks new readers.
Mechanism 5: Once, WaitGroup, and the things that must not be copied
sync.Once is free after the first call. Measured: 1.87 ns against 1.92 for
a hand-rolled atomic check and 2.23 for doing nothing at all — all three within
noise of each other. So there is nothing to gain by replacing Once with a
manual flag, while it is easy to get the manual version wrong: a second
goroutine wedges in between the check and the set.
And Once gives what a manual flag does not give by itself:
For any call to once.Do(f), f is synchronized before the return from any call of
once.Do(f).
Read that as: a goroutine that sees the initialisation already done is guaranteed to see its result. A manual flag without atomic operations gives no such guarantee — and that is a correctness bug, not a performance one.
WaitGroup: two rules. Add is called before launching the goroutine,
not inside it — otherwise Wait may get there first. And the counter must not
go negative: an extra Done is a panic.
And the things that must not be copied. Mutex, RWMutex, Once and
WaitGroup all carry the same sentence in their documentation:
A Mutex must not be copied after first use.
The reason is that a copy does not inherit state: a copy of a locked mutex
arrives free, and two goroutines enter the critical section at once. The run
shows it: a struct with a mutex, copied by assignment, then lives its own life —
1 for the original and 2 for the copy.
go vet catches this with the copylocks check — "check for locks erroneously
passed by value". It is in the set go test runs by default, and the message
reads passes lock by value. The three most common places: taking a struct by
value, a value receiver on a method, and assignment.
Deeper: cache lines — why parallelism is not free
There is one more reason "just put an atomic on every field" does not give the expected speed-up, and it lives below the level of the language — in the hardware.
A processor does not work with bytes but with cache lines — blocks of 64 bytes. If two counters updated by different cores land in the same line, every write by one invalidates the line for the other, and it is shuttled between cores:
This is called false sharing, and its symptom is recognisable: cores were added and nothing got faster — sometimes it got slower. The cure is separating them into different lines (the standard library has padding fields for this) or having each core accumulate its own counter and summing them at the end.
Practically, one thing is needed from this: atomic removes the lock but does not make the write free. A happens-before edge still requires agreement between cores — just through the cache-coherence protocol rather than through the scheduler.
And this section has the same kind of boundary as the numbers above. Sixty-four bytes is a property of the processor rather than of Go, and on another machine the line size may differ. So the rule is written from the symptom, not from the number: if adding cores does not speed things up, separate the counters — whatever the line size on that particular machine turns out to be.
How to answer in an interview
Short answer: n++ is three actions, not one — read, add, write; two
goroutines wedge in between each other's steps and an update is lost. From
there the choice is simple: a mutex protects the integrity of a region of
code — any number of fields and any logic between them — while an atomic
operation fits bounded state updated by a single indivisible operation: a
counter, a flag, a pointer. Neither of them buys speed; both buy a guarantee
that one goroutine sees another's work whole rather than half-done.
That is enough for a correct answer. What follows is what you add when the interviewer digs.
If the interviewer digs deeper
To "what is wrong with n++" answer with three operations. "Read, add,
write; two updates merge into one and the total comes out short — eight
goroutines of 10,000 gave me less than 80,000."
On atomic and mutex, talk about the scope of protection, not speed. "An atomic protects one cell and one action; a mutex a region of code. As soon as you must read one thing and write another, an atomic does not fit regardless of speed."
To "which is faster" ask back. "At what contention? Without it a mutex is twice as dear; with sixteen goroutines on one counter it is nearly an order of magnitude, because it parks the loser. And if the contention is that high, that is what to fix, not the primitive."
On RWMutex, talk about section length. "RLock is dearer than Lock, so on
a short section the RWMutex loses; the gain appears once enough time is spent
under the lock."
On Once, name the guarantee rather than the price. "It gives
happens-before: whoever sees the flag sees the initialisation result too. Its
fast path is free anyway."
On copying, name the tool. "A copy of a mutex arrives unlocked; go vet
catches it with copylocks, which already runs in go test."
Next they ask
What is sync.Map and when is it needed?
A map with built-in synchronisation, built for two narrow cases: an entry for a key is written once and read many times (a cache that only grows), or different goroutines work with disjoint sets of keys.
Outside those two it usually loses to a plain map under an RWMutex, and it
always gives up type safety: its keys and values are interfaces. Its own
documentation advises a plain map with locking by default — one of the rare
cases where the right interview answer is written down in the docs.
Why sync.Pool if there is a garbage collector?
To reuse objects that are expensive to create — buffers, slices, parsers —
and take some work off the allocator. The classic use is a bytes.Buffer in an
HTTP handler.
Two things to know about it. First: the pool's contents are cleared on garbage
collection, so it is neither a cache nor a connection pool — nothing may be
counted on surviving. Second: an object comes out of the pool in the state it
was returned in, and resetting it is the taker's job. A forgotten Reset is the
source of the nastiest bugs, where one user's data ends up in another user's
response.
How does atomic.Value differ from atomic.Pointer?
atomic.Value came first and stores an any, so it checks the type at run
time: putting values of different types into it panics. atomic.Pointer[T]
arrived in Go 1.19, is typed and is checked by the compiler.
In practice, publishing a configuration snapshot today uses
atomic.Pointer[Config]: it is both safer and free of boxing the value into an
interface. atomic.Value remains in older code and where the type genuinely is
not known at compile time.
Is a mutex fair? Who gets the lock next?
sync.Mutex has two modes. In the normal one whoever gets lucky takes it — and
that is fast, because a goroutine already on the CPU does not pay for a switch.
But if a goroutine has waited longer than a millisecond, the mutex enters
starvation mode: the lock is handed directly to the first in the queue, and
new contenders join the queue rather than trying to snatch it.
Hence the practical part: you cannot build logic on the order of acquisition, but
you need not fear indefinite starvation either — the runtime cures it itself. And
the boundary: the two modes and the one-millisecond threshold are the current
implementation of sync.Mutex (checked on go1.24.7) rather than a promise of the
language; the language guarantees no acquisition order at all, which is exactly
why nothing is built on it.
How do you limit the number of concurrent operations?
With a buffered channel as a semaphore — that is the idiom:
sem := make(chan struct{}, 10)
for _, job := range jobs {
sem <- struct{}{}
go func(j Job) {
defer func() { <-sem }()
handle(j)
}(job)
}The channel's capacity is the concurrency limit. The alternatives are
golang.org/x/sync/semaphore with weights, if the operations differ in
"heaviness", or errgroup.SetLimit if you also want the first error collected.
What not to do is roll your own counter on an atomic: waiting for a slot to free
correctly does not work out that way.
What happens on a second Unlock?
A panic: sync: unlock of unlocked mutex. A Go mutex counts no nesting levels
and remembers no owner — it is not recursive.
Two things follow. First: taking the same mutex twice in one goroutine is a
deadlock, not "re-entry". Second: Unlock may be called from a different
goroutine than Lock — the language does not forbid it; that is occasionally
deliberate, but usually a sign of tangled ownership.
Common misconceptions
n++ is atomic: it is one operation in the source
It is three: read, add, write. Two updates merge into one — the run gives less than 80,000 where exactly that was expected. One line in the source says nothing about indivisibility.
atomic is just a fast mutex
The difference is not speed but the scope of protection: an atomic covers one cell and one action, a mutex a region of code of any length. "Read A, decide, write B" cannot be replaced by atomicity at all.
a mutex is twice as dear as an atomic
That holds only without contention: 13.35 ns against 6.49. As contenders grow from 2 to 16 the mutex becomes 9.6 times dearer while the atomic becomes 1.0, and the ratio reaches 8.7. "Which is faster" has no answer without "at what contention".
an atomic has no cost under contention
It has, just a different one. It parks no goroutine, but the cell is shared and the cache line under it travels between cores. If sixteen goroutines fight over one cell, what to fix is the contention — a counter per goroutine, sharding — not the primitive.
many reads means an RWMutex
On a short section an RWMutex loses: RLock does more work than Lock. Measured: with an empty section, 98.78 ns against 24.01. The gain appears only on a long one — 671.01 against 1463.55.
sync.Once is expensive, a flag is better
After the first call it is free: 1.87 ns — within noise of doing nothing. And more importantly it gives happens-before: whoever sees the flag is guaranteed to see the initialisation result. A manual flag without atomics gives no such guarantee, and that is a correctness bug rather than a performance one.
a struct with a mutex can be passed by value
A copy does not inherit state: a copy of a locked mutex arrives free, and the protection vanishes silently. Mutex, RWMutex, Once and WaitGroup all carry the same sentence — must not be copied after first use. go vet catches it with copylocks.
a mutex protects a variable
It protects a region of code, and its link to a variable exists only in the author's head. If even one goroutine reads the field outside the lock there are no guarantees — and the memory model states this through happens-before, not through "the variable is protected".
Practice
Two problems. Answer first, then check against the real output: in both, the correct answer is taken from a run of the script rather than assigned.
Practice · predict the output
fmt.Println(unsynced()) fmt.Println(syncedAtomic()) fmt.Println(onceCount()) a, b := copiedStruct() fmt.Println(a, b)
Practice · estimate
Knowledge check
Eight goroutines do 10,000 increments each of a plain int variable. What ends up in it?
This is neither a retelling nor a separate text: everything below is taken from the article itself — its own summary, the section headings, the “actually” column and the version table. Which is why these theses cannot drift from the article.
The gist
- A plain
n++from several goroutines loses updates, because it is not one action but three: read, add, write. Two goroutines manage to read the same number, both add one, both write — and one of the additions disappears. Synchronisation exists not for speed but to stop that happening. - Hence the main consequence: the loss does not happen every time — and that is worse than if it always did. The code "works" until the load changes: in one run, eight goroutines of 10,000 increments produced exactly 80,000, meaning nothing was lost at all. And hence, too, the division of labour worth stating out loud: a mutex protects the integrity of a region of code — any number of fields and any logic between them; an atomic operation fits where the state is bounded and changes in one indivisible action.
- Beyond that come the numbers, and they overturn two common pieces of advice. The guarantees, meanwhile, come not from the word "lock" but from the memory model: happens-before — which is why "I put a mutex on it, so it is safe" holds only if all access goes through it. Without contention a mutex is twice as dear as an atomic — 13.35 ns against 6.49; but as contenders grow from 2 to 16 the mutex becomes 9.6 times dearer while the atomic becomes 1.0: the mutex parks the loser, the atomic operation retries. "Use an RWMutex for reads" is wrong on a short section: with an empty section
RLockloses toLock(98.78 against 24.01 ns), and the gain appears only on a long one — 671.01 against 1463.55.sync.Onceis free after the first call — 1.87 ns, within noise of doing nothing at all. And a copy of a struct holding a mutex is a different mutex;go vetcatches it with thecopylockscheck, which already runs insidego test. The numbers were taken on one machine and one version of Go: what is stable is the directions and the ratios, not the nanoseconds themselves.
In fact
- It is three: read, add, write. Two updates merge into one — the run gives less than 80,000 where exactly that was expected. One line in the source says nothing about indivisibility.
- The difference is not speed but the scope of protection: an atomic covers one cell and one action, a mutex a region of code of any length. "Read A, decide, write B" cannot be replaced by atomicity at all.
- That holds only without contention: 13.35 ns against 6.49. As contenders grow from 2 to 16 the mutex becomes 9.6 times dearer while the atomic becomes 1.0, and the ratio reaches 8.7. "Which is faster" has no answer without "at what contention".
- It has, just a different one. It parks no goroutine, but the cell is shared and the cache line under it travels between cores. If sixteen goroutines fight over one cell, what to fix is the contention — a counter per goroutine, sharding — not the primitive.
- On a short section an RWMutex loses:
RLockdoes more work thanLock. Measured: with an empty section, 98.78 ns against 24.01. The gain appears only on a long one — 671.01 against 1463.55. - After the first call it is free: 1.87 ns — within noise of doing nothing. And more importantly it gives happens-before: whoever sees the flag is guaranteed to see the initialisation result. A manual flag without atomics gives no such guarantee, and that is a correctness bug rather than a performance one.
- A copy does not inherit state: a copy of a locked mutex arrives free, and the protection vanishes silently.
Mutex,RWMutex,OnceandWaitGroupall carry the same sentence — must not be copied after first use.go vetcatches it withcopylocks. - It protects a region of code, and its link to a variable exists only in the author's head. If even one goroutine reads the field outside the lock there are no guarantees — and the memory model states this through happens-before, not through "the variable is protected".
What is covered
- What is really being asked
- Base: a data race — and why `n++` is not one action
- Mechanism 1: the guarantee comes from happens-before, not from the lock
- Mechanism 2: atomic is not "a fast mutex"
- Mechanism 3: "which is faster" is an incomplete question
- Mechanism 4: an RWMutex is not a free replacement for a Mutex
- Mechanism 5: Once, WaitGroup, and the things that must not be copied
- Deeper: cache lines — why parallelism is not free
- How to answer in an interview
- Next they ask
- Common misconceptions
- Practice
- Knowledge check
Sources & further reading
4 SOURCES
- The Go Memory Model — SynchronizationOfficial documentation. The foundation the whole topic rests on: the guarantees come not from «locks» but from happens-before relations. On mutexes: «For any sync.Mutex or sync.RWMutex variable l and n < m, call n of l.Unlock() is synchronized before call m of l.Lock() returns». On atomics: «The APIs in the sync/atomic package … behave like a sequentially consistent atomic operation». On Once: «For any call to once.Do(f), f is synchronized before the return from any call of once.Do(f)».https://go.dev/ref/mem
- The sync package — Mutex, RWMutex, Once, WaitGroupOfficial documentation. The rule half the mistakes grow from: «A Mutex must not be copied after first use» — the same sentence appears on RWMutex, Once and WaitGroup. On RWMutex it is also stated that the lock is not recursive: «If any goroutine calls Lock while the lock is already held by one or more readers, concurrent calls to RLock will block until the writer has acquired (and released) the lock».https://pkg.go.dev/sync
- The sync/atomic packageOfficial documentation. The caveat worth being able to quote: «These functions require great care to be used correctly. Except for special, low-level applications, synchronization is better done with channels or the facilities of the sync package». And on the types that replaced the functions: «The swap operation, implemented by the SwapT functions, is the atomic equivalent of: old = *addr; *addr = new; return old».https://pkg.go.dev/sync/atomic
- go vet — the copylocks checkOfficial documentation. The tool that catches the quietest bug in this topic. The check's description: «check for locks erroneously passed by value». It is part of the set `go test` runs by default — so most people already have it running, and its message `passes lock by value` is worth being able to read.https://pkg.go.dev/cmd/vet