or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

errgroup.mdindex.mdsemaphore.mdsingleflight.mdsyncmap.md
tile.json

syncmap.mddocs/

Syncmap - Concurrent Map

The syncmap package provides a concurrent map implementation. It is a type alias to sync.Map from the Go standard library, which was originally prototyped in this package before being added to the standard library in Go 1.9.

Import

import "golang.org/x/sync/syncmap"

Or use the standard library directly:

import "sync"

// Then use sync.Map instead of syncmap.Map

Overview

The Map type is designed for concurrent use by multiple goroutines without additional locking. It's optimized for two common scenarios:

  1. Write-once, read-many: Entry for a given key is written once but read many times
  2. Disjoint key sets: Multiple goroutines read, write, and overwrite entries for disjoint sets of keys

For other scenarios, a regular map with separate sync.RWMutex may have better performance.

API Reference

Map Type

type Map = sync.Map

Map is a concurrent map with amortized-constant-time loads, stores, and deletes. It is safe for multiple goroutines to call a Map's methods concurrently.

Zero value:

  • The zero Map is valid and empty
  • Ready to use without initialization

Important:

  • A Map must not be copied after first use
  • Use pointers to Map instead of copying the value

Methods

Since Map is a type alias to sync.Map, it inherits all methods from the standard library. Here are the available operations:

Load

func (m *Map) Load(key any) (value any, ok bool)

Loads the value stored in the map for a key, or nil if no value is present.

Parameters:

  • key: The key to look up (any comparable type)

Returns:

  • value: The value stored for the key, or nil
  • ok: true if the key was present, false otherwise

Example:

var m syncmap.Map
m.Store("key", "value")

value, ok := m.Load("key")
if ok {
    fmt.Println(value.(string))  // "value"
}

Store

func (m *Map) Store(key, value any)

Stores a value for a key.

Parameters:

  • key: The key to store (any comparable type)
  • value: The value to store (any type)

Example:

var m syncmap.Map
m.Store("user:123", &User{ID: 123, Name: "Alice"})

LoadOrStore

func (m *Map) LoadOrStore(key, value any) (actual any, loaded bool)

Loads a value if present, otherwise stores the provided value.

Parameters:

  • key: The key to look up or store
  • value: The value to store if key is not present

Returns:

  • actual: The value now stored for the key (either existing or newly stored)
  • loaded: true if the value was loaded (existed), false if it was stored (new)

Use cases:

  • Atomic get-or-create operations
  • Ensuring singleton initialization

Example:

var m syncmap.Map

// First call: stores and returns the value
actual, loaded := m.LoadOrStore("key", "value1")
fmt.Println(actual, loaded)  // "value1" false

// Second call: returns existing value
actual, loaded = m.LoadOrStore("key", "value2")
fmt.Println(actual, loaded)  // "value1" true

LoadAndDelete

func (m *Map) LoadAndDelete(key any) (value any, loaded bool)

Loads a value and deletes it atomically.

Parameters:

  • key: The key to load and delete

Returns:

  • value: The value that was stored, or nil
  • loaded: true if the key was present, false otherwise

Example:

var m syncmap.Map
m.Store("key", "value")

value, loaded := m.LoadAndDelete("key")
fmt.Println(value, loaded)  // "value" true

// Key is now deleted
value, loaded = m.Load("key")
fmt.Println(value, loaded)  // nil false

Delete

func (m *Map) Delete(key any)

Deletes the value for a key.

Parameters:

  • key: The key to delete

Behavior:

  • Does nothing if the key is not present
  • No return value

Example:

var m syncmap.Map
m.Store("key", "value")
m.Delete("key")

Range

func (m *Map) Range(f func(key, value any) bool)

Calls f sequentially for each key and value present in the map.

Parameters:

  • f: Function called for each entry. Return false to stop iteration.

Behavior:

  • Iteration order is not specified
  • If f returns false, iteration stops
  • Range does not block other operations on the map
  • If entries are added, modified, or removed during iteration, they may or may not be visited

Example:

var m syncmap.Map
m.Store("key1", "value1")
m.Store("key2", "value2")

m.Range(func(key, value any) bool {
    fmt.Printf("%s: %s\n", key, value)
    return true  // Continue iteration
})

CompareAndSwap

func (m *Map) CompareAndSwap(key, old, new any) bool

Swaps the value for a key if the current value equals old.

Parameters:

  • key: The key to update
  • old: The expected current value
  • new: The new value to store

Returns:

  • bool: true if the swap occurred, false otherwise

Use cases:

  • Atomic compare-and-swap operations
  • Implementing optimistic locking
  • Lock-free data structures

Example:

var m syncmap.Map
m.Store("counter", 0)

// Try to update from 0 to 1
swapped := m.CompareAndSwap("counter", 0, 1)
fmt.Println(swapped)  // true

// Try to update from 0 to 2 (fails because value is now 1)
swapped = m.CompareAndSwap("counter", 0, 2)
fmt.Println(swapped)  // false

CompareAndDelete

func (m *Map) CompareAndDelete(key, old any) bool

Deletes the entry for a key if the current value equals old.

Parameters:

  • key: The key to delete
  • old: The expected current value

Returns:

  • bool: true if the deletion occurred, false otherwise

Example:

var m syncmap.Map
m.Store("key", "value")

// Try to delete if value is "value"
deleted := m.CompareAndDelete("key", "value")
fmt.Println(deleted)  // true

// Key is now gone
value, ok := m.Load("key")
fmt.Println(ok)  // false

Swap

func (m *Map) Swap(key, value any) (previous any, loaded bool)

Swaps the value for a key and returns the previous value.

Parameters:

  • key: The key to swap
  • value: The new value to store

Returns:

  • previous: The previous value, or nil if key didn't exist
  • loaded: true if the key existed, false if this is a new entry

Example:

var m syncmap.Map
m.Store("key", "value1")

previous, loaded := m.Swap("key", "value2")
fmt.Println(previous, loaded)  // "value1" true

value, _ := m.Load("key")
fmt.Println(value)  // "value2"

Usage Examples

Basic Concurrent Access

package main

import (
    "fmt"
    "golang.org/x/sync/syncmap"
    "sync"
)

func main() {
    var m syncmap.Map
    var wg sync.WaitGroup

    // Multiple goroutines writing
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()
            m.Store(id, fmt.Sprintf("value-%d", id))
        }(i)
    }

    // Multiple goroutines reading
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()
            if value, ok := m.Load(id); ok {
                fmt.Printf("Loaded: %s\n", value)
            }
        }(i)
    }

    wg.Wait()
}

Concurrent Cache

package main

import (
    "fmt"
    "golang.org/x/sync/syncmap"
    "time"
)

type Cache struct {
    data syncmap.Map
}

func (c *Cache) Get(key string) (string, bool) {
    value, ok := c.data.Load(key)
    if !ok {
        return "", false
    }
    return value.(string), true
}

func (c *Cache) Set(key, value string) {
    c.data.Store(key, value)
}

func (c *Cache) GetOrSet(key string, fn func() string) string {
    // Try to load first
    if value, ok := c.Get(key); ok {
        return value
    }

    // Compute value
    value := fn()

    // Store and return (might store duplicate if concurrent, but that's ok)
    actual, _ := c.data.LoadOrStore(key, value)
    return actual.(string)
}

func main() {
    cache := &Cache{}

    // Concurrent access
    for i := 0; i < 100; i++ {
        go func(id int) {
            key := fmt.Sprintf("key-%d", id%10)
            value := cache.GetOrSet(key, func() string {
                time.Sleep(10 * time.Millisecond)
                return fmt.Sprintf("computed-%s", key)
            })
            fmt.Println(value)
        }(i)
    }

    time.Sleep(time.Second)
}

Atomic Counter Pattern

package main

import (
    "fmt"
    "golang.org/x/sync/syncmap"
    "sync"
)

type Counter struct {
    m syncmap.Map
}

func (c *Counter) Increment(key string) int {
    for {
        value, ok := c.m.Load(key)
        if !ok {
            // Try to initialize to 1
            if _, loaded := c.m.LoadOrStore(key, 1); !loaded {
                return 1
            }
            continue
        }

        current := value.(int)
        next := current + 1

        if c.m.CompareAndSwap(key, current, next) {
            return next
        }
        // CAS failed, retry
    }
}

func (c *Counter) Get(key string) int {
    value, ok := c.m.Load(key)
    if !ok {
        return 0
    }
    return value.(int)
}

func main() {
    counter := &Counter{}
    var wg sync.WaitGroup

    // 100 goroutines incrementing same counter
    for i := 0; i < 100; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            counter.Increment("requests")
        }()
    }

    wg.Wait()
    fmt.Printf("Final count: %d\n", counter.Get("requests"))  // 100
}

Registry Pattern

package main

import (
    "fmt"
    "golang.org/x/sync/syncmap"
)

type ServiceRegistry struct {
    services syncmap.Map
}

type Service interface {
    Start() error
    Stop() error
}

func (r *ServiceRegistry) Register(name string, service Service) error {
    _, loaded := r.services.LoadOrStore(name, service)
    if loaded {
        return fmt.Errorf("service %s already registered", name)
    }
    return nil
}

func (r *ServiceRegistry) Get(name string) (Service, bool) {
    value, ok := r.services.Load(name)
    if !ok {
        return nil, false
    }
    return value.(Service), true
}

func (r *ServiceRegistry) Unregister(name string) bool {
    _, loaded := r.services.LoadAndDelete(name)
    return loaded
}

func (r *ServiceRegistry) All() []Service {
    var services []Service
    r.services.Range(func(key, value any) bool {
        services = append(services, value.(Service))
        return true
    })
    return services
}

Event Handlers Map

package main

import (
    "fmt"
    "golang.org/x/sync/syncmap"
)

type EventBus struct {
    handlers syncmap.Map  // map[string][]func(interface{})
}

func (e *EventBus) Subscribe(event string, handler func(interface{})) {
    for {
        value, ok := e.handlers.Load(event)
        if !ok {
            // Initialize handler slice
            handlers := []func(interface{}){handler}
            if _, loaded := e.handlers.LoadOrStore(event, handlers); !loaded {
                return
            }
            continue
        }

        handlers := value.([]func(interface{}))
        newHandlers := append(handlers, handler)

        if e.handlers.CompareAndSwap(event, handlers, newHandlers) {
            return
        }
        // CAS failed, retry
    }
}

func (e *EventBus) Publish(event string, data interface{}) {
    value, ok := e.handlers.Load(event)
    if !ok {
        return
    }

    handlers := value.([]func(interface{}))
    for _, handler := range handlers {
        go handler(data)
    }
}

func main() {
    bus := &EventBus{}

    bus.Subscribe("user.created", func(data interface{}) {
        fmt.Printf("Handler 1: %v\n", data)
    })

    bus.Subscribe("user.created", func(data interface{}) {
        fmt.Printf("Handler 2: %v\n", data)
    })

    bus.Publish("user.created", map[string]string{"id": "123", "name": "Alice"})
}

Iterator with Early Exit

package main

import (
    "fmt"
    "golang.org/x/sync/syncmap"
)

func main() {
    var m syncmap.Map

    // Populate map
    for i := 0; i < 100; i++ {
        m.Store(i, i*i)
    }

    // Find first value > 100
    found := false
    var result int

    m.Range(func(key, value any) bool {
        v := value.(int)
        if v > 100 {
            result = v
            found = true
            return false  // Stop iteration
        }
        return true  // Continue
    })

    if found {
        fmt.Printf("Found: %d\n", result)
    }
}

Performance Considerations

When to Use sync.Map

sync.Map (and syncmap.Map) is optimized for specific use cases:

  1. Write-once, read-many workloads: Entries are written once and read repeatedly
  2. Disjoint key sets: Different goroutines work with different keys

When NOT to Use sync.Map

For general-purpose concurrent map access, especially with frequent writes to the same keys, a regular map with sync.RWMutex may perform better:

type SyncMap struct {
    mu sync.RWMutex
    m  map[string]interface{}
}

func (s *SyncMap) Load(key string) (interface{}, bool) {
    s.mu.RLock()
    defer s.mu.RUnlock()
    v, ok := s.m[key]
    return v, ok
}

func (s *SyncMap) Store(key string, value interface{}) {
    s.mu.Lock()
    defer s.mu.Unlock()
    s.m[key] = value
}

Best Practices

  1. Use type assertions carefully: All values are any, always check types:

    value, ok := m.Load(key)
    if !ok {
        return ErrNotFound
    }
    str, ok := value.(string)
    if !ok {
        return ErrWrongType
    }
  2. Don't copy Map: Maps must not be copied after first use. Always use pointers:

    // Wrong
    func process(m syncmap.Map) { }
    
    // Correct
    func process(m *syncmap.Map) { }
  3. Consider LoadOrStore for initialization: Avoid race conditions during initialization:

    // Race condition
    if _, ok := m.Load(key); !ok {
        m.Store(key, initialize())  // Multiple goroutines might initialize
    }
    
    // Atomic
    m.LoadOrStore(key, initialize())
  4. Use Range sparingly: Range iterates over all entries, which can be expensive for large maps. Consider whether you need a different data structure if you frequently iterate.

  5. Be aware of Range consistency: Entries added/removed during Range may or may not be visited. Don't rely on seeing a consistent snapshot.

  6. Use CompareAndSwap for atomic updates: When updating values based on their current state:

    for {
        old, ok := m.Load(key)
        if !ok {
            old = defaultValue
        }
        new := compute(old)
        if m.CompareAndSwap(key, old, new) {
            break
        }
    }

Relationship to Standard Library

This package provides syncmap.Map as a type alias to sync.Map. They are identical:

import (
    "sync"
    "golang.org/x/sync/syncmap"
)

// These are the same type
var m1 sync.Map
var m2 syncmap.Map

// Can assign freely
m1 = m2
m2 = m1

You can use either import, but sync.Map from the standard library is more common in modern Go code. The syncmap package exists for historical compatibility, as this package was the original prototype for sync.Map.