# Go - Concurrency, Locks, and checklocks
Shared data in Go fails in a few recurring ways: torn reads of unsynchronized strings, silent success on the panic path, slice aliasing, and circular deadlock from nested locks. This page collects one working pattern per failure. Runtime helpers and the checklocks static analyzer verify the lock discipline behind each pattern.
## Key points
- `atomic.Value` fits write-once, read-many package-level globals that concurrent goroutines access. It avoids mutex overhead and satisfies `-race`.
- `atomic.Pointer.Swap` gives a read-and-clear, runs-once primitive: it returns the old value and stores `nil` in one step. Of two callers that race for the function, only one gets the non-nil value and executes it.
- `atomic.Pointer.CompareAndSwap(nil, &fn)` gives first-wins activation: when two goroutines race to activate, only one succeeds and the call of the loser is a no-op.
- Two separate atomic loads of the same pointer do not guarantee the same value. Hoist the load into one local variable, so all dependent fields come from one consistent snapshot.
- An unsynchronized plain `string` cache is a torn-read hazard: a concurrent reader can see a non-zero length with a nil data pointer, and the comparison or hash that follows faults (`SIGSEGV` on arm64). `atomic.Pointer[string]` fixes this and keeps the caching semantics unchanged.
- `unsafe.Pointer` conversions between `string` and `[]byte` that skip the copy violate the string immutability guarantee of Go. They fail under the race detector and under future GC changes. Use `string(b)` and `[]byte(s)`. The compiler often elides the copy in hot paths.
- A result write outside the mutex before `close(done)` is memory-model-safe: the `close` happens-before any receive that unblocks, and the write is sequenced before the `close`. Moving the write inside the mutex is an unnecessary fix.
- A `close(done)` without a sentinel turns the panic path into silent success: joiners unblock with a nil result and a nil error, and default error handling returns success, for example HTTP 200 with an empty body. One sentinel assignment before the `close` turns the failure loud.
- A panic-absorbing upstream changes what a panic test must exercise: when the common path already converts collector panics to errors, the test must use a directly-panicking `TransactionalGatherer`, never a panicking collector.
- `errgroup.WithContext` returns a derived context that is cancelled when any goroutine returns an error. A discarded derived context gives the goroutines no automatic cancellation. Always pass the returned context, not the parent.
- `sync.Map.Delete` on a missing key is a documented no-op, so call it unconditionally in cleanup defers. A guard branch costs more than the call.
- Reset a `sync.Once` by assignment, `once = sync.Once{}`, only under a mutex. The pattern is unusual enough to require an explicit comment, or it reads as a bug.
- A shared-field write from a request-setup hook races with the unlocked read of the same field by the server. `-race` proves this in a unit repro and in an end-to-end burst. The race-free shape configures the matched handler per request instead.
- A lock release between two dependent reads opens a TOCTOU window. Do both reads under one lock acquisition.
- The `chan chan struct{}` reply-channel pattern coordinates flushes across several independent implementations. Reuse it. Do not invent a new mechanism.
- Write retry loops as `attempt <= retries`, not `attempt < retries`. This form gives exactly one attempt when `retries = 0`. State the invariant and keep every call site identical.
- `append(s[:i], s[i+1:]...)` mutates the backing array of the caller, because the sub-slice shares it. Use `slices.Delete`, the in-place shrink of Go 1.21+, or allocate a new slice.
- A conditional lock helper, `withLockIf(condition, f)`, handles the "this code may already hold the lock" case in a partial-flush path. It avoids a double lock without a separate boolean flag.
- `assert.RWMutexRLocked(&mu)` and `assert.RWMutexLocked(&mu)` verify lock preconditions at runtime in locked-suffix helpers. Add them when the naming convention alone does not satisfy the static analyzer.
- `assert.RWMutexLocked` asserts the write lock. `assert.RWMutexRLocked` asserts read-or-write. A read-only method called under `RLock` must use the latter, or it panics at runtime.
- Annotate atomic-only fields with `// +checkatomic`. Always use `atomic.StoreUint32` and `atomic.LoadUint32` on them, including in `clear()` and `reset()` paths that look like single-threaded initialization.
- A `+checklocksread:mu` annotation on a method means callers must hold `mu.RLock()` before the call. checklocks enforces this statically, not at runtime.
- Fields protected by the mutex of a type stay off-limits outside its methods, even in the constructor. Constructor code is not exempt from checklocks.
- A second lock acquired while a trace-wide lock is already held is enough to create circular deadlock. Release the first lock before any peer span lock.
- Type-alias mutexes in the default build keep checklocks annotations on struct fields working unchanged, so the migration friction is zero.
- Code generators such as msgp produce files that need blanket `+checklocksignore` annotations, applied by a post-generation script. Hand-annotating regenerated output does not survive the next generation run.
- A build tag with no positive counterpart silently skips the lock assertions. Check both sides of every build tag, or the asymmetry hides the lock checks entirely.
## How to apply
1. Store write-once, read-many globals in `atomic.Value` or `atomic.Pointer[T]`, not behind a mutex.
2. Implement runs-once, read-and-clear with `Swap`, and first-wins activation with `CompareAndSwap(nil, &fn)`.
3. Load a shared pointer once into a local, and read every dependent field from that snapshot.
4. Wrap any `string` that concurrent goroutines read and write in `atomic.Pointer[string]`.
5. Set an error sentinel before every `close(done)` on a panic path, so joiners see the failure.
6. Pass the context returned by `errgroup.WithContext` to every goroutine in the group.
7. Call `sync.Map.Delete` unconditionally in cleanup defers.
8. Do dependent reads under one lock acquisition. Never release between them.
9. Shrink a slice with `slices.Delete` or a fresh allocation when callers keep aliases.
10. Match the lock assertion to the lock the caller holds: `RWMutexRLocked` for read-or-write, `RWMutexLocked` for write-only.
11. Keep atomic fields atomic in `clear()` and `reset()` paths, and annotate them with `+checkatomic`.
12. Apply `+checklocksignore` to generated files through a post-generation script, and check both sides of every build tag that guards lock assertions.
## Observations
- [concurrency] `atomic.Pointer.Swap` stores `nil` while it returns the old value, so only one of two racing callers executes the function. #go (source: agent sessions, 2026-06)
- [memory-model] A torn plain `string` read faults on arm64. `atomic.Pointer[string]` removes the race. #go (source: agent sessions, 2026-09)
- [concurrency] A `close(done)` without a sentinel turns a panic into silent success, for example HTTP 200 with an empty body. #go (source: agent sessions, 2026-07)
- [locks] `withLockIf(condition, f)` replaces a boolean flag in a partial-flush path that may already hold the lock. #go (source: agent sessions, 2026-01)
- [static-analysis] `+checklocksread:mu` makes the caller-lock precondition a static check, not a runtime one. #go (source: agent sessions, 2026-02)
- [locks] A second span lock taken under a trace-wide lock creates circular deadlock. Release the trace-wide lock first. #go (source: agent sessions, 2026-01)
- [slices] `append(s[:i], s[i+1:]...)` mutates the backing array of the caller. `slices.Delete` avoids the aliasing. #go (source: agent sessions, 2026-03)
- [concurrency] A retry loop written as `attempt <= retries` gives exactly one attempt when `retries = 0`. #go (source: agent sessions, 2026-03)
## Relations
- relates_to [[resources/documentation/Go/Go - Correctness Idioms]] (overlapping Go correctness rules)
- relates_to [[resources/documentation/Go/Go - Testing Patterns and Tests That Certify Nothing]] (race-detector verification and panic-path tests)
- relates_to [[resources/documentation/Go/Instrumentation/Go Instrumentation - Context Propagation Across Goroutines]] (errgroup derived-context cancellation)
- relates_to [[resources/concepts/Go Programming Expertise]] (broader Go domain synthesis)
## Sources
Personal agent-session notes, January 2026 to October 2026