# go.uber.org/atomic: typed wrappers that make atomic access the only access

> The atomic package from Uber wraps Go's primitive types so that a variable of a given type can only be read and written atomically, which the type system enforces rather than a code review, and it adds the arithmetic and comparison helpers that the standard library's raw functions leave to the caller. It suits a codebase that has been bitten by a plain variable shared between goroutines, and it is a small, stable dependency with a single import path rule to remember.

**uber-go/atomic** — Wrapper types for sync/atomic which enforce atomic access

- Repository: https://github.com/uber-go/atomic
- Website: https://go.uber.org/atomic
- Stars: 1,457 · Forks: 118
- Language: Go
- License: MIT
- Published: 2026-10-16 · Updated: 2026-10-16 · Language: en
- Canonical page: https://hysenlabs.com/projects/uber-go-atomic

## The problem it solves

The library describes itself as simple wrappers for primitive types to enforce atomic access, and the reasoning is given in one sentence. The standard library's sync/atomic is powerful, but it is easy to forget which variables must be accessed atomically, and the package preserves all the functionality of the standard library while wrapping the primitive types to provide a safer, more convenient API.

That is the whole idea, and the difference from the raw library is one of type, not of mechanism. In the standard library, atomicity is a property of the operation you choose. A variable declared as an int is an int, and the compiler will let you read it directly, which is a data race unless you remember not to. Here, the variable has its own type, and the only way to get a value out of it is through an atomic method, so an accidental plain read is a compile error rather than a race that the race detector may or may not catch for you.

The API is small enough to hold in your head.

```go
var atom atomic.Uint32
atom.Store(42)
atom.Sub(2)
atom.CompareAndSwap(40, 11)
```

Store, an arithmetic operation and a compare and swap, and each of them is a method on the value rather than a function taking a pointer to it. That is the ergonomic difference, and the safety difference is that you cannot take the address of the value and hand it to something that will read it plainly, because the type is not the underlying primitive type and the compiler will not let you mix them.

The development status in the readme is one word, Stable, and the licence is MIT.

## What is actually wrapped, and how the types are laid out

The repository root is a long list of files, and the naming pattern is the documentation of what the package covers. There is a file per primitive, with bool.go, duration.go, error.go, float32.go, float64.go, int32.go, int64.go, string.go, time.go, uint32.go, uint64.go, uintptr.go, pointer.go, value.go and unsafe_pointer.go, and next to most of them a matching extension file whose name ends in _ext.

The split between the two files is the interesting part. The plain file holds the atomic access methods, and the extension file holds the convenience operations that build on them, the arithmetic and the comparisons and the string handling. The same pairing is visible in the test files, with a test file for each, and there is a stress test file and an example test file and an assert test file at the top level.

The presence of nocmp.go is worth a note, because it is the guard on the type safety. A wrapper around a primitive that accidentally acquired a compare and swap method or a comparison operator would let two different atomic variables be compared, or compared to a plain value, and that would break the guarantee the package exists to provide. The no comparison file is how that is prevented.

The duration and time wrappers are the ones people reach for first in a server, because a counter that is bumped from several request handlers is the canonical case, and the pointer and value wrappers cover the cases where the atomic thing is a struct or a slice header rather than a number.

## The import path rule, and the escape hatch for old code

There is a section of the readme devoted to the legacy import path, and it is the first thing that will bite anyone upgrading an older dependency.

As of v1.5.0, the import path go.uber.org/atomic is the only supported way of using the package. If you are using Go modules, the package will fail to compile with the legacy import path github.com/uber-go/atomic. The readme recommends migrating, and then gives the fallback for the case where you cannot, which is that some of your dependencies still use the old path. In that case you have to add a replace directive to your module file downgrading the legacy import path to an older version.

```
replace github.com/uber-go/atomic => github.com/uber-go/atomic v1.4.0
```

And there is a command that does it for you.

```shell
$ go mod edit -replace github.com/uber-go/atomic=github.com/uber-go/atomic@v1.4.0
```

This is the kind of note that only exists because a project changed its identity without breaking its users, and it is a real cost of adopting it: if a dependency of yours still imports the old path, you are pinning that dependency's copy of the library to an old version, and you now have two copies of a package with the same types in your build, which is a class of bug that is annoying to find.

The installation itself is the usual Go get with the major version pinned, which is the modern form that makes the v2 module path possible in the first place.

## The code generation, and what the Makefile enforces

Two generator programs in the internal directory are the source of the type files, and the Makefile shows how they are built and used.

There is a generator for the atomic integer wrappers and one for the atomic wrapper generator, and the generate target runs go generate across the tree. The coverage configuration then excludes both generator packages from the coverage report, and the readme's comment in the Makefile notes that the ignore section in the codecov configuration file must be updated to match.

So the per-type files are not hand written. A generator produces them, which is the only way to keep a dozen primitive types consistent with each other, and the generator packages are excluded from coverage because the code they emit is what is covered.

The rest of the Makefile is a quality gate with some teeth. The test target runs the suite with the race detector enabled, which for a package about atomicity is the only test that matters. There are separate targets for the format check, the linter and the static analysis, and the lint target runs all of them plus a generation check.

That last one is the interesting target. The generatenodirty target fails if the working tree is dirty after generation, which means the committed files always match what the generator produces, and a contributor who changes a template and forgets to regenerate gets a failing build rather than a review comment. The other tools, the linter and the static checker, are installed into a tools directory on demand rather than being assumed to be present, so a fresh clone builds the same way on every machine.

## What it does not do, and the state of the project

Two things to be clear about before you adopt it.

The first is scope. The package is wrappers around primitives and the operations on them. It is not a general concurrency library, it does not provide channels or pools or contexts, and it does not solve the problem of a shared struct where only one field needs to be atomic. For a struct, the value wrapper and the pointer wrapper cover the cases where the whole thing is replaced atomically, and the documentation is the reference for the exact set of methods on each type.

The second is the relationship with the standard library. The package preserves all the functionality of the standard library, so nothing here is unavailable elsewhere, and the question is only whether you want the type enforcement and the extra helpers on top of it. A codebase that has already standardised on the typed atomics in the standard library will find much of the value duplicated.

On the state of the project, the release history is a bit lopsided in a useful way. The versions are 1.12.0 from September 2026, 1.11.0 from May 2023 and 1.10.0 from August 2022, and the last push to the repository was on 2026-09-21, so the recent work went into a single release after a long quiet period. The development status says stable, and the module file asks for Go 1.20 or later, which is a floor low enough to be unproblematic.

The dependency footprint is also minimal. The module requires the testify assertion library and one indirect YAML library, and nothing else, which for a library that every goroutine using a counter might import is exactly what you want.

## Conclusion

Adopt go.uber.org/atomic if your code shares counters, flags or values between goroutines and you want the compiler to enforce that every access is atomic, because a distinct type per variable is a stronger guarantee than a convention about which functions to call. Do not adopt it in a codebase that already standardises on the typed atomics in the standard library, since the value it adds there is the extra helpers rather than the safety. Before you migrate, check the import path, because the legacy path no longer compiles with Go modules and the fix is a replace directive in your module file.

## FAQ

### What does go.uber.org/atomic add to sync/atomic?

It preserves all the functionality of the standard library but wraps the primitive types, so a variable of type atomic.Uint32 can only be read and written through atomic methods. The readme describes the motivation as it being easy to forget which variables must be accessed atomically, and the type system makes the wrong access a compile error.

### How do I install the atomic package?

With go get -u go.uber.org/atomic@v1, which installs the module under its supported import path. The development status is stated as stable and the licence is MIT.

### What is the legacy import path problem with go.uber.org/atomic?

Since version 1.5.0 the only supported import path is go.uber.org/atomic, and with Go modules the package fails to compile under the legacy path github.com/uber-go/atomic. If a dependency still uses the old path you can pin it with a replace directive downgrading to version 1.4.0, which the go mod edit command can add for you.

### How is the atomic package tested and generated?

The per-type files are produced by generators that run through go generate, and the Makefile has a target that fails if the working tree is dirty after generation, so the committed files always match the generator. The test target runs the suite with the race detector enabled, alongside format, lint and static analysis checks.

## Sources

- [License: MIT](https://github.com/uber-go/atomic/blob/master/LICENSE)
- [Project website](https://go.uber.org/atomic)
- [README](https://github.com/uber-go/atomic/blob/master/README.md)
- [Releases](https://github.com/uber-go/atomic/releases)
- [uber-go/atomic on GitHub](https://github.com/uber-go/atomic)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/uber-go-atomic
