Why could atomic.AddInt64 on a struct field panic on 32-bit builds, and what does atomic.Int64 change?
answer
- the old calls take a raw address
- 32-bit targets align int64 on four bytes
- only the first word of a struct is promised
- it panics at run time, not at build time
- the newer type brings its own alignment
basics
~20 sOn 32-bit platforms a 64-bit atomic operation requires an 8-byte-aligned address, and only the first word of an allocated struct is guaranteed to be aligned, so a later int64 field can panic at run time. The atomic.Int64 type carries that alignment itself, wherever the field sits.
solid answer
~50 sThe older package-level API takes a raw address: `atomic.AddInt64(&s.hits, 1)`. On 32-bit architectures a plain `int64` field only needs 4-byte alignment, but a 64-bit atomic instruction needs the address to be 8-byte aligned, so an `int64` placed after a `uint32` in a struct can land on a 4-byte boundary and the call panics with an unaligned 64-bit atomic operation. The rule `sync/atomic` documents is narrow: the first word of an allocated struct, array or slice, and of a global variable, can be relied on to be 64-bit aligned — nothing else. The old workaround was to declare the `int64` first or pad around it, and to keep that ordering forever. Since Go 1.19 you declare the field as `atomic.Int64`; the type embeds its own alignment guarantee, so it is safe anywhere in the struct, and its value is unexported so nothing can read it non-atomically either.
code
go · 13 lines// Function API: hits is only 4-byte aligned on 32-bit builds,
// so atomic.AddInt64(&c.hits, 1) panics there.
type countersOld struct {
flag uint32
hits int64
}
// Typed API (Go 1.19+): alignment is part of the type,
// and the value is unexported so nothing can touch it non-atomically.
type countersNew struct {
flag uint32
hits atomic.Int64
}go deeper
Know that sync/atomic has an older function form taking a pointer and a newer typed form, and that the typed one is what to reach for in new code. Recognise that 32-bit targets have an alignment requirement the types handle for you.
Explain the mechanics: why a 64-bit atomic operation needs an 8-byte-aligned address, that only the first word of an allocation is guaranteed aligned, and that the failure is a run-time panic on 32-bit builds only.
Treat it as a layout invariant that a later refactor will break, and say how you would prevent that: migrate to the typed atomics, or write the requirement down and build a 32-bit target in CI. Be clear about what go vet does and does not catch here.
Decide whether 32-bit targets are still in scope at all, and make that call explicit rather than implicit. If they are, own the CI cost of proving it; if they are not, say so in writing so the next engineer does not carry a phantom constraint.
## Two APIs in one package `sync/atomic` has carried two styles for years. The **function API** operates on an address you hand it: `atomic.LoadInt64(&x)`, `atomic.StoreInt64(&x, v)`, `atomic.AddInt64(&x, 1)`, `atomic.SwapInt64(&x, v)`, `atomic.CompareAndSwapInt64(&x, old, new)`, with parallel families for `Int32`, `Uint32`, `Uint64`, `Uintptr` and `Pointer`. The variable itself is an ordinary `int64`. The **typed API**, added in **Go 1.19**, gives you `atomic.Int64` (and `Int32`, `Uint32`, `Uint64`, `Bool`, `Pointer[T]`), a struct whose unexported value is reachable only through `Load`, `Store`, `Add`, `Swap` and `CompareAndSwap` methods. The alignment trap belongs to the first style. ## What alignment has to do with it A 64-bit atomic instruction operates on an aligned 8-byte word. On 64-bit architectures that costs nothing: the compiler already aligns `int64` to 8 bytes, so every `int64` you can address is a valid target. On 32-bit architectures — 32-bit x86, 32-bit ARM, 32-bit MIPS — the natural alignment of `int64` is only 4 bytes, so a field can legitimately sit at an address that is a multiple of 4 but not of 8. ```go type counters struct { flag uint32 // 4 bytes hits int64 // starts 4 bytes in: NOT 8-byte aligned on a 32-bit build } ``` Call `atomic.AddInt64(&c.hits, 1)` on such a build and the program panics at run time with an unaligned 64-bit atomic operation. It is not a compile error and it is not a silent wrong answer — it is a crash, and one that never appears on the developer's 64-bit machine. The guarantee `sync/atomic` actually documents is deliberately narrow: **the first word of an allocated struct, array or slice, and of a global variable, can be relied upon to be 64-bit aligned.** That is all. A second `int64` field, an element in the middle of a struct, or a field after a `uint32` gets no promise on a 32-bit target. ## How people used to cope Three workarounds were standard, and all three are fragile: 1. **Put the `int64` first** in the struct, so it occupies the first word. 2. **Pad explicitly**, e.g. a `_ [4]byte` after a `uint32`, to push the `int64` onto an 8-byte boundary. 3. **Allocate it separately** and hold a `*int64`, so the value is at the start of its own allocation. Each one is a layout invariant that lives in the reviewer's head. The next person to add a field, reorder for readability, or run a struct-packing tool re-breaks it, and the failure only shows up on a platform CI may not even build for. It is a classic "correct today, broken by an unrelated refactor tomorrow" hazard. ## What the typed atomic fixes `atomic.Int64` and `atomic.Uint64` embed an alignment marker as part of the type, so a field of that type is 64-bit aligned no matter where it appears in a struct and no matter what precedes it. The layout invariant becomes the type's problem instead of the author's: ```go type counters struct { flag uint32 hits atomic.Int64 // aligned wherever it sits } ``` The second, quieter benefit is encapsulation. With `atomic.AddInt64(&c.hits, 1)` the field is a plain `int64`, and any other line in the package can still write `c.hits++` or read `c.hits` directly — an atomic write racing a non-atomic read is still a data race. With `atomic.Int64` the value is unexported, so every access goes through a method and the mixed-access bug becomes unwritable. A third point is that the typed atomics must not be copied after first use. Copying one gives you a second, independent counter; that is a different bug from the alignment one, but the same discipline — hold the struct by pointer — prevents both. ## What tooling does and does not catch `go vet` includes an `atomic` check, but its subject is a different mistake: assigning the result of an atomic operation back to the same variable, as in ```go x = atomic.AddInt64(&x, 1) // go vet: direct assignment to atomic value ``` which re-introduces a non-atomic store and throws away the atomicity you asked for. What `go vet` does **not** do is check 64-bit alignment. Alignment depends on the target architecture and on the struct layout at the point of use, and the default vet suite makes no such report. So the old API's trap is invisible to the standard toolchain: the only reliable signals are building and running the test suite for a 32-bit `GOARCH`, or removing the hazard by using the typed atomics. On a codebase that must keep the function API — because it works with an `int64` it does not own, for instance — the discipline is to write the alignment requirement down beside the struct definition and to test on a 32-bit target in CI. Otherwise, migrating the field to `atomic.Int64` deletes the whole category.
- Which alignment does sync/atomic actually guarantee, and for what?Only the first word of an allocated struct, array or slice, and of a global variable, is guaranteed to be 64-bit aligned. Everything else — a second int64 field, a field following a uint32, an interior element — has no promise on a 32-bit target. That narrowness is why the historical fixes were all about forcing the value into a first-word position.
- What does go vet's atomic check actually report, and does it cover alignment?It reports assigning an atomic operation's result back to the same variable, such as `x = atomic.AddInt64(&x, 1)`, which adds a non-atomic store and defeats the point. It says nothing about 64-bit alignment: that depends on the target architecture and the struct layout, and no default vet analyser checks it. The alignment trap is invisible to the standard toolchain.
- Beyond alignment, why is atomic.Int64 safer than a plain int64 used with atomic.AddInt64?Because the value is unexported. With a plain field, some other line can still write `c.hits++` or read `c.hits` directly, and an atomic write racing a non-atomic read is a data race that compiles cleanly. Wrapping it in atomic.Int64 makes every access go through a method, so the mixed-access bug cannot be written in the first place.
saying these in an interview costs you the question
- Says Go aligns every int64 field to eight bytes everywhere
- Expects the compiler to reject the unaligned atomic access
- Thinks the unaligned call silently degrades to a normal add
- Believes go vet flags 64-bit atomic alignment problems
- Assumes the trap is gone because nobody ships 32-bit builds
- Fixes it by reordering fields and leaves no note for the next editor