The config swap problem
Your Go service handles thousands of requests per second. A configuration value needs to update without restarting the process. You could use a mutex to protect the config, but every single request would acquire and release that lock. Lock contention creates latency. You need a way to swap the configuration instantly so readers never block. sync/atomic.Value provides a lock-free mechanism to store and load a single value that changes infrequently.
Imagine a digital sign at a gas station. The price changes. The sign does not flicker through intermediate prices. It shows the old price, then instantly shows the new price. Drivers never see a corrupted price. atomic.Value works the same way. It swaps the entire value in one step. Readers see either the old value or the new value. They never see a half-written state.
How atomic.Value works
sync/atomic.Value is a thread-safe container for a single value. The type is any, so it can hold anything. The magic is in the Store and Load methods. These operations are atomic. Atomic means the operation completes as a single indivisible step. No other goroutine can observe a partial update.
The underlying implementation uses CPU atomic instructions to swap a pointer. On most architectures, this is a single machine instruction that guarantees the swap happens without interruption. Because there is no lock, readers never block. The read path is extremely fast. The cost is that you must type assert the result of Load, and you cannot mutate the value inside the container. You must create a new value and store it.
Convention aside: atomic.Value lives in the sync/atomic package. The package name groups all atomic operations. The type Value is a struct. You usually declare it as a package-level variable or embed it in a struct. The receiver naming convention does not apply here because Value is a value type, not a method on a custom type.
Minimal example
Here is the simplest usage: store a string, load it, and assert the type.
package main
import (
"fmt"
"sync/atomic"
)
// value holds a string that can be swapped safely.
var value atomic.Value
func main() {
// Store the initial configuration.
// Store accepts any type.
value.Store("v1")
// Load returns the current value.
// The result is an interface{}, so we assert the type.
v := value.Load().(string)
fmt.Println(v)
}
What happens under the hood
When you call Store, the value is boxed into an interface. The underlying pointer is swapped using a CPU atomic instruction. Load reads the pointer atomically and returns the interface. You must type assert the result. If the types do not match, the program panics.
The compiler does not check the type of Store against Load. atomic.Value is untyped at compile time. The type safety is enforced at runtime by the type assertion. If you store a string and try to assert to an int, the program crashes.
Convention aside: if err != nil is verbose by design. Type assertions are similar. The explicit assertion value.Load().(string) makes the type requirement visible. The community accepts the boilerplate because it prevents silent type errors.
Realistic usage: hot-reloading config
A common pattern is a configuration struct that updates when a file changes. The config is read by every request handler. You want to swap the config without locking the handlers.
Here is the config struct and the update function.
// Config holds service settings.
type Config struct {
Port int
Mode string
}
var config atomic.Value
// updateConfig swaps the config atomically.
func updateConfig(newCfg Config) {
// Store replaces the value.
// Safe to call concurrently with Load.
config.Store(newCfg)
}
Here is the read function. It handles the case where the value has not been stored yet.
// getConfig reads the current config without locking.
func getConfig() Config {
// Load fetches the value atomically.
// Returns nil if Store was never called.
v := config.Load()
if v == nil {
return Config{}
}
// Type assertion is required because Value stores any.
return v.(Config)
}
The updateConfig function creates a new Config struct and stores it. The getConfig function loads the value and asserts it. If the config has not been initialized, Load returns nil. The function returns a zero-value config in that case.
Pitfalls and runtime panics
atomic.Value has three common traps. The first is a type mismatch. If you store a string and load it as an int, the program panics with panic: interface conversion: interface {} is string, not int. The compiler cannot catch this error because Store accepts any. You must ensure the types match at runtime.
The second trap is storing nil after a non-nil value. atomic.Value tracks the type of the first stored value. If you store a string, you can only store strings or nil. If you store a string, then store an int, the program panics with panic: sync/atomic: store of inconsistently typed value into Value. This check prevents type confusion. Once you store a non-nil value, the type is fixed. You cannot change the type later.
The third trap is mutating the value inside the container. atomic.Value protects the pointer, not the data. If you store a map and mutate the map, readers see the mutation. The map pointer is atomic, but the map contents are not. You must create a new map and store the new map. This is the immutable update pattern. You copy the data, modify the copy, and store the copy.
Convention aside: atomic.Value is not a map. It is a single slot. Do not try to store a map and mutate the map contents. The map pointer is atomic, the contents are not. If you mutate the map, readers see partial updates. You must create a new map and Store the new map.
Decision matrix
Use atomic.Value when you have a single value that changes rarely but is read constantly, and you want to avoid mutex overhead on the read path. Use atomic.Pointer[T] when you know the concrete type at compile time and want to avoid the type assertion overhead and runtime panic risk of atomic.Value. Use a sync.Mutex when you need to protect multiple related values or perform read-modify-write sequences that require consistency across fields. Use a channel when you need to coordinate events or pass data between goroutines with backpressure, rather than just sharing a variable.
atomic.Pointer[T] was added in Go 1.19. It is the modern replacement for atomic.Value when the type is known. atomic.Pointer stores a pointer to a concrete type. The Store and Load methods are typed. The compiler checks the type. There is no type assertion. There is no runtime panic for type mismatch. The performance is better because there is no interface boxing. If you are writing new code and the type is known, use atomic.Pointer. Use atomic.Value only when the type is truly dynamic or when you need to support older Go versions.
Atomic swaps are fast. Type assertions are not free.