gosync.Map is a generic, concurrent map with the semantics of sync.Map — and
without its cost: keys and values are stored as they are, so there is no
interface boxing on the way in and no type assertion on the way out.
It is the hash-trie behind sync.Map since Go 1.24 (internal/sync.HashTrieMap),
ported to user code and made generic.
The usual way to get a typed sync.Map is a thin generic wrapper:
type Map[K comparable, V any] struct{ m sync.Map }
func (m *Map[K, V]) Load(key K) (V, bool) {
v, ok := m.m.Load(key) // key boxed into an interface
return v.(V), ok // value asserted back
}Every call pays for the boxing, the type assertion and the dynamic type checks
inside sync.Map. gosync.Map removes all of it: the trie stores K and V
directly and hashes keys with hash/maphash.Comparable.
gosync.Map[K, V] |
wrapper over sync.Map |
|
|---|---|---|
| Type safety | compile time | runtime assertions |
| Load / LoadOrStore (hit) | lock-free, 0 allocs | lock-free, 0 allocs, boxing on every call |
| Store (existing key) | 1 alloc (new entry node) | 1–2 allocs (entry + boxed key) |
| Iteration | All() for range, Range() |
Range() with any |
| Values | any type; == via ComparableMap |
any |
The same workloads where sync.Map is the right tool:
- caches and registries that are read far more often than written;
- maps where disjoint goroutines write disjoint keys.
For write-heavy, high-churn maps (frequent stores, deletes, TTL evictions) use goshard: a sharded, lock-based map that allocates nothing on stores.
- Drop-in for
sync.Map—Load,Store,LoadOrStore,LoadAndDelete,Delete,Swap,CompareAndSwap,CompareAndDelete,Range,Clear, with the same semantics and the same concurrency guarantees. - Zero-allocation reads —
Load,LoadOrStoreof an existing key,Deleteof a missing key,Range,LenandEmptynever allocate. - Go 1.23+ iteration —
for k, v := range m.All(). LenandEmpty—Emptystops at the first entry;Lenwalks the map.ComparableMap—CompareAndSwapandCompareAndDeleteby==;Mapoffers them with an equality function, so values need not be comparable.LoadOrCompute— get-or-create where the value is built only when the key is missing, and only once even when goroutines race for the same key (LoadOrStore(key, newValue())builds a value per caller and keeps one).- Reads of a zero map are free — a
Mapthat was never written is not initialized byLoad,RangeorDelete. - Zero dependencies — only the standard library.
go get github.com/darxnet/gosyncRequires Go 1.24 (hash/maphash.Comparable).
// The zero value is ready to use.
var m gosync.Map[string, *Session]
m.Store("sid-1", s)
if s, ok := m.Load("sid-1"); ok {
fmt.Println(s.User)
}
// Get-or-create: the value is built only when missing, once per key.
s, loaded := m.LoadOrCompute("sid-2", newSession)
// Iterate.
for id, s := range m.All() {
fmt.Println(id, s.User)
}
// Compare-and-swap by == needs comparable values.
var status gosync.ComparableMap[string, string]
status.Store("worker-1", "idle")
status.CompareAndSwap("worker-1", "idle", "busy") // true
// ...or by an equality function on any value type.
var versions gosync.Map[string, []byte]
versions.CompareAndSwap("cfg", old, new, bytes.Equal)Environment: Apple M3 Pro · darwin/arm64 · Go 1.26
go test -bench=. -benchmem -count=6 -cpu=1,4,8 ./..., summarised withbenchstat. Maps hold 16K entries withuint32keys and pointer values unless noted.sync.Map wrapperis the generic wrapper from the section above;RWMutex mapis a plain map behindsync.RWMutex.
| Implementation | 1 CPU | 4 CPUs | 8 CPUs | Allocs/op |
|---|---|---|---|---|
gosync.Map |
14.1 ns | 3.9 ns | 2.2 ns | 0 |
sync.Map wrapper |
17.5 ns | 4.8 ns | 2.7 ns | 0 |
RWMutex map |
9.0 ns | 33.7 ns | 91.8 ns | 0 |
gosync.Map is 19% faster than the wrapper on every CPU count: that is the boxing and the type
assertion. String keys show the same gap (16.4 vs 20.0 ns at 1 CPU); a map of 128K string keys loads
in 20.2 vs 24.9 ns.
| Implementation | 1 CPU | 4 CPUs | 8 CPUs | Allocs/op |
|---|---|---|---|---|
gosync.Map |
15.1 ns | 4.2 ns | 2.3 ns | 0 |
sync.Map wrapper |
28.0 ns | 7.8 ns | 4.6 ns | 0 (3 B/op) |
RWMutex map |
15.9 ns | 100.7 ns | 120.4 ns | 0 |
The wrapper boxes both the key and the value on every call, so LoadOrStore costs 2x there.
| Implementation | Store 1 CPU | Store 8 CPUs | Store allocs | Insert | Insert allocs |
|---|---|---|---|---|---|
gosync.Map |
47.7 ns | 13.9 ns | 1 (32 B) | 260 ns | 1 (85 B) |
sync.Map wrapper |
64.5 ns | 19.2 ns | 1 (51 B) | 338 ns | 2 (105 B) |
RWMutex map |
12.7 ns | 129.8 ns | 0 | 119 ns | 0 (72 B) |
A store publishes a new entry node, so that lock-free readers never see a torn value: that is the one
allocation, the same as in sync.Map. The wrapper adds the boxed key on top of it.
| Implementation | 1 CPU | 4 CPUs | 8 CPUs | Allocs/op |
|---|---|---|---|---|
gosync.Map |
39.1 ns | 179 ns | 207 ns | 1 |
sync.Map wrapper |
57.8 ns | 200 ns | 229 ns | 1-2 |
RWMutex map |
30.2 ns | 163 ns | 185 ns | 0 |
This is the workload sync.Map is not made for, and gosync.Map inherits that: a plain mutex map
wins, and goshard wins by far more. Use gosync.Map for
read-mostly maps.
Range over 16K entries takes about 0.2 ms for both trie maps and 0.1 ms for a plain map; Range,
Len and Empty allocate nothing.
The sibling modules follow the same rule — nothing allocated on the hot path, the zero value ready to use:
- goshard — sharded, lock-based concurrent map for high-churn data: stores allocate nothing
- goreset — generates
Reset()for pooled structs: back to the zero state, memory kept - gocopy — generates
CopyFrom/Copythat reuse the destination's memory instead of allocating
Apache License 2.0. See LICENSE for details.
map.go, map_test.go and export_test.go are derived from the Go standard
library (src/internal/sync/hashtriemap.go and its tests), Copyright 2024 The
Go Authors, under the BSD 3-Clause License — see LICENSE-GO and
NOTICE.