gosync

package module
v1.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 12, 2026 License: Apache-2.0 Imports: 5 Imported by: 0

README

gosync

Go Reference Go Coverage

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.

Why gosync?

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

When to use it

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.

Features

  • 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, LoadOrStore of an existing key, Delete of a missing key, Range, Len and Empty never allocate.
  • Go 1.23+ iteration — for k, v := range m.All().
  • Len and Empty — Empty stops at the first entry; Len walks the map.
  • ComparableMap — CompareAndSwap and CompareAndDelete by ==; Map offers 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 Map that was never written is not initialized by Load, Range or Delete.
  • Zero dependencies — only the standard library.

Installation

go get github.com/darxnet/gosync

Requires Go 1.24 (hash/maphash.Comparable).

Quick Start

// 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)

Benchmarks

Environment: Apple M3 Pro · darwin/arm64 · Go 1.26 go test -bench=. -benchmem -count=6 -cpu=1,4,8 ./..., summarised with benchstat. Maps hold 16K entries with uint32 keys and pointer values unless noted. sync.Map wrapper is the generic wrapper from the section above; RWMutex map is a plain map behind sync.RWMutex.

Load (existing keys)
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.

LoadOrStore (existing keys, as a warm cache)
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.

Store (existing keys) and Insert (new keys)
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.

Write-Read-Delete cycle (high churn)
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.

Iteration

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.

See also

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/Copy that reuse the destination's memory instead of allocating

License

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.

Documentation

Overview

Package gosync provides Map, a generic concurrent map with the semantics of sync.Map and without its cost: no interface boxing of keys and values and no type assertions on the way out.

Map is the hash-trie behind sync.Map since Go 1.24 (internal/sync.HashTrieMap), ported to user code: it hashes keys with hash/maphash.Comparable and locks with sync.Mutex. Loads are lock-free and never allocate; a store allocates one node for the entry, which readers may see immediately.

Map fits the same workloads as sync.Map: caches and registries that are read far more often than written, or where disjoint sets of goroutines write disjoint keys. For write-heavy, high-churn maps see github.com/darxnet/goshard.

ComparableMap adds CompareAndSwap and CompareAndDelete by == for comparable values; Map offers them with an equality function. Map.LoadOrCompute builds a missing value once per key, whatever the number of goroutines asking.

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type ComparableMap

type ComparableMap[K comparable, V comparable] struct {
	Map[K, V]
}

ComparableMap is a Map whose values are comparable, so CompareAndSwap and CompareAndDelete can compare them with == instead of an equality function.

Example (CompareAndDelete)

ExampleComparableMap_compareAndDelete deletes a key only when its value matches the expected one.

package main

import (
	"fmt"

	"github.com/darxnet/gosync"
)

func main() {
	var m gosync.ComparableMap[string, int]
	m.Store("session", 42)

	fmt.Println(m.CompareAndDelete("session", 0))  // wrong value — no-op
	fmt.Println(m.CompareAndDelete("session", 42)) // matches — deleted

	_, ok := m.Load("session")
	fmt.Println(ok)

}
Output:
false
true
false
Example (CompareAndSwap)

ExampleComparableMap_compareAndSwap demonstrates compare-and-swap by == .

package main

import (
	"fmt"

	"github.com/darxnet/gosync"
)

func main() {
	var m gosync.ComparableMap[string, string]
	m.Store("status", "idle")

	// Succeeds: current value matches "idle".
	fmt.Println(m.CompareAndSwap("status", "idle", "busy"))

	// Fails: current value is "busy", not "idle".
	fmt.Println(m.CompareAndSwap("status", "idle", "busy"))

}
Output:
true
false

func (*ComparableMap[K, V]) CompareAndDelete

func (m *ComparableMap[K, V]) CompareAndDelete(key K, old V) (deleted bool)

CompareAndDelete deletes the entry for key if its value is equal to old.

If there is no current value for key in the map, CompareAndDelete returns false.

func (*ComparableMap[K, V]) CompareAndSwap

func (m *ComparableMap[K, V]) CompareAndSwap(key K, old, new V) (swapped bool)

CompareAndSwap swaps the old and new values for key if the value stored in the map is equal to old.

If there is no current value for key in the map, CompareAndSwap returns false.

type Map

type Map[K comparable, V any] struct {
	// contains filtered or unexported fields
}

Map is a concurrent hash-trie with the semantics of sync.Map: it is safe for use by multiple goroutines without additional locking, loads are lock-free, and the iteration order is unspecified.

Map is designed around frequent loads, but offers decent performance for stores and deletes as well, especially if the map is larger.

The zero Map is empty and ready to use. It must not be copied after first use.

Example

ExampleMap demonstrates basic store, load, and delete operations. The zero value is ready to use; no constructor call is required.

package main

import (
	"fmt"

	"github.com/darxnet/gosync"
)

func main() {
	var m gosync.Map[string, int]

	m.Store("active_users", 1024)

	if v, ok := m.Load("active_users"); ok {
		fmt.Println(v)
	}

	m.Delete("active_users")

	if _, ok := m.Load("active_users"); !ok {
		fmt.Println("gone")
	}

}
Output:
1024
gone
Example (CompareAndSwap)

ExampleMap_compareAndSwap compares values with a function, which lets a Map hold values that are not comparable.

package main

import (
	"fmt"

	"github.com/darxnet/gosync"
)

func main() {
	var m gosync.Map[string, []int]
	m.Store("ids", []int{1, 2})

	sameLen := func(current, old []int) bool { return len(current) == len(old) }

	fmt.Println(m.CompareAndSwap("ids", []int{0, 0}, []int{1, 2, 3}, sameLen))
	fmt.Println(m.CompareAndSwap("ids", []int{0, 0}, []int{9}, sameLen))

}
Output:
true
false
Example (LoadOrCompute)

ExampleMap_loadOrCompute builds the value only when the key is missing, and only once even if several goroutines ask for it at the same time.

package main

import (
	"fmt"

	"github.com/darxnet/gosync"
)

// Session is the value type of the examples below.
type Session struct {
	User string
}

func main() {
	var sessions gosync.Map[string, *Session]

	s, loaded := sessions.LoadOrCompute("sid-1", func() *Session {
		fmt.Println("creating sid-1")

		return &Session{User: "ann"}
	})
	fmt.Println(s.User, loaded)

	s, loaded = sessions.LoadOrCompute("sid-1", func() *Session {
		fmt.Println("never called")

		return nil
	})
	fmt.Println(s.User, loaded)

}
Output:
creating sid-1
ann false
ann true
Example (LoadOrStore)

ExampleMap_loadOrStore demonstrates get-or-set semantics: the first call stores the value, the second returns the existing one.

package main

import (
	"fmt"

	"github.com/darxnet/gosync"
)

func main() {
	var m gosync.Map[string, int]

	actual, loaded := m.LoadOrStore("limit", 100)
	fmt.Println(actual, loaded) // stored

	actual, loaded = m.LoadOrStore("limit", 999)
	fmt.Println(actual, loaded) // existing value returned

}
Output:
100 false
100 true

func (*Map[K, V]) All

func (m *Map[K, V]) All() iter.Seq2[K, V]

All returns an iterator over each key and value present in the map.

The iterator does not necessarily correspond to any consistent snapshot of the Map's contents: no key will be visited more than once, but if the value for any key is stored or deleted concurrently (including by yield), the iterator may reflect any mapping for that key from any point during iteration. The iterator does not block other methods on the receiver; even yield itself may call any method on the Map.

Example

ExampleMap_All iterates with a range-over-func loop.

package main

import (
	"fmt"

	"github.com/darxnet/gosync"
)

func main() {
	var m gosync.Map[int, string]
	m.Store(1, "one")

	for k, v := range m.All() {
		fmt.Println(k, v)
	}

}
Output:
1 one

func (*Map[K, V]) Clear

func (m *Map[K, V]) Clear()

Clear deletes all the entries, resulting in an empty Map.

func (*Map[K, V]) CompareAndDelete

func (m *Map[K, V]) CompareAndDelete(key K, old V, eq func(current, old V) bool) (deleted bool)

CompareAndDelete deletes the entry for key if its value is equal to old according to eq. The eq function is called while the node of the key is locked.

If there is no current value for key in the map, CompareAndDelete returns false.

func (*Map[K, V]) CompareAndSwap

func (m *Map[K, V]) CompareAndSwap(key K, old, new V, eq func(current, old V) bool) (swapped bool)

CompareAndSwap swaps the old and new values for key if the value stored in the map is equal to old according to eq. The eq function is called while the node of the key is locked.

If there is no current value for key in the map, CompareAndSwap returns false.

func (*Map[K, V]) Delete

func (m *Map[K, V]) Delete(key K)

Delete deletes the value for a key.

func (*Map[K, V]) Empty

func (m *Map[K, V]) Empty() bool

Empty reports whether the map has no entries. It stops at the first entry found, so it is cheap on a non-empty map.

func (*Map[K, V]) Len

func (m *Map[K, V]) Len() int

Len returns the number of entries in the map. It walks the whole map, with the same consistency as Range: entries stored or deleted concurrently may or may not be counted.

func (*Map[K, V]) Load

func (m *Map[K, V]) Load(key K) (value V, ok bool)

Load returns the value stored in the map for a key, or the zero value if no value is present. The ok result indicates whether value was found in the map.

func (*Map[K, V]) LoadAndDelete

func (m *Map[K, V]) LoadAndDelete(key K) (value V, loaded bool)

LoadAndDelete deletes the value for a key, returning the previous value if any. The loaded result reports whether the key was present.

func (*Map[K, V]) LoadOrCompute added in v1.1.0

func (m *Map[K, V]) LoadOrCompute(key K, compute func() V) (actual V, loaded bool)

LoadOrCompute returns the existing value for the key if present. Otherwise, it calls compute, stores its result and returns it. The loaded result is true if the value was loaded, false if computed.

compute runs at most once per key for concurrent callers: the key's bucket is locked while it runs, so it must be short and must not use m. LoadOrStore(key, newValue()) in contrast may build values that are then thrown away.

func (*Map[K, V]) LoadOrStore

func (m *Map[K, V]) LoadOrStore(key K, value V) (actual V, loaded bool)

LoadOrStore returns the existing value for the key if present. Otherwise, it stores and returns the given value. The loaded result is true if the value was loaded, false if stored.

func (*Map[K, V]) Range

func (m *Map[K, V]) Range(f func(K, V) bool)

Range calls f sequentially for each key and value present in the map. If f returns false, range stops the iteration.

This exists for compatibility with sync.Map; All should be preferred. It provides the same guarantees as sync.Map, and All.

func (*Map[K, V]) Store

func (m *Map[K, V]) Store(key K, value V)

Store sets the value for a key.

func (*Map[K, V]) Swap

func (m *Map[K, V]) Swap(key K, value V) (previous V, loaded bool)

Swap swaps the value for a key and returns the previous value if any. The loaded result reports whether the key was present.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL