A thread-safe, generic ordered map for Go that maintains insertion order while providing O(1) operations for lookups, deletes, and moves.
- Insertion order preservation - Iterates in the order items were added (unlike Go's built-in maps)
- O(1) operations - Fast lookups, deletes, and moves using map + doubly-linked list
- Thread-safe - All operations use internal locking (RWMutex)
- Zero-value usable - No constructor required:
var om OrderedMap[K,V]
just works - Generic - Works with any comparable key type and any value type
- Snapshot-based iteration - Range/RangeBreak take snapshots, preventing deadlocks even if callbacks modify the map
go get github.com/DocSpring/orderedmap
package main
import (
"fmt"
"github.com/DocSpring/orderedmap"
)
func main() {
// Create a new ordered map
om := orderedmap.NewOrderedMap[string, int]()
// Or use zero value (no constructor needed)
var om2 orderedmap.OrderedMap[string, int]
// Add entries
om.Set("zebra", 26)
om.Set("alpha", 1)
om.Set("beta", 2)
// Get values (O(1) lookup)
val, ok := om.Get("alpha")
fmt.Println(val, ok) // 1 true
// Iterate in insertion order
om.Range(func(key string, value int) {
fmt.Printf("%s: %d\n", key, value)
})
// Output:
// zebra: 26
// alpha: 1
// beta: 2
}
Go's built-in map[K]V
has random iteration order by design. This causes problems when you need:
- Stable UI rendering - Prevent flickering when displaying map contents in a terminal UI
- Predictable output - Generate consistent results across runs
- FIFO/insertion-order semantics - Process items in the order they were added
- Deterministic testing - Avoid flaky tests from random map iteration
OrderedMap uses a map + doubly-linked list hybrid approach:
- *map[K]node - O(1) lookups by key
- Doubly-linked list - O(1) deletes and moves, preserves insertion order
- Cached length - O(1) len() operation
This combination provides optimal performance for all operations except indexed access (At), which is O(n).
// Constructor (recommended)
om := orderedmap.NewOrderedMap[string, int]()
// Zero value (also works)
var om orderedmap.OrderedMap[string, int]
// Set - Insert or update (O(1))
om.Set("key", 42)
// Get - Retrieve value (O(1))
val, ok := om.Get("key")
// Has - Check existence (O(1))
if om.Has("key") { ... }
// Delete - Remove entry (O(1))
deleted := om.Delete("key")
// DeleteWithValue - Remove entry and return its value (O(1))
val, deleted := om.DeleteWithValue("key")
// Len - Get size (O(1))
count := om.Len()
// Range - Iterate over all entries (insertion order)
om.Range(func(key string, value int) {
fmt.Printf("%s: %d\n", key, value)
})
// RangeBreak - Iterate with early exit
om.RangeBreak(func(key string, value int) bool {
fmt.Printf("%s: %d\n", key, value)
return key != "stop" // return false to break
})
Important: Range and RangeBreak take a snapshot before iterating, so the callback can safely modify the map without causing deadlocks.
Note: The snapshot copies both keys and values. For value types (int, struct, etc.), mutations after the snapshot are not reflected in the iteration. For pointer or reference types (*struct, map, slice), changes to the underlying data will be visible.
// RangeLocked - Iterate without allocating a snapshot
om.RangeLocked(func(key string, value int) {
// Process items
})
RangeLocked
MUST NOT call any OrderedMap methods or deadlock will occur. Use Range()
if you need to modify the map during iteration.
// Front - Get first entry (O(1))
key, val, ok := om.Front()
// Back - Get last entry (O(1))
key, val, ok := om.Back()
// At - Get entry at index (O(n))
key, val, ok := om.At(5)
// PopFront - Remove and return first entry (O(1))
key, val, ok := om.PopFront()
// PopBack - Remove and return last entry (O(1))
key, val, ok := om.PopBack()
// MoveToEnd - Move key to end of order (O(1))
moved := om.MoveToEnd("key")
// GetOrSet - Atomic get-or-create (O(1))
val, existed := om.GetOrSet("key", func() int {
return expensiveComputation()
})
mk
function in GetOrSet
is called while holding the write lock. Keep it fast and simple to avoid blocking other operations.
// Keys - Get all keys (returns copy)
keys := om.Keys()
// Values - Get all values (returns copy)
values := om.Values()
// Clear - Remove all entries (preserves capacity)
om.Clear()
// Reset - Remove all entries and free memory
om.Reset()
Operation | Complexity | Notes |
---|---|---|
Set | O(1) | Amortized due to map growth |
Get | O(1) | Hash map lookup |
Has | O(1) | Hash map lookup |
Delete | O(1) | Doubly-linked list removal |
DeleteWithValue | O(1) | Returns value during removal |
PopFront | O(1) | Remove head of list |
PopBack | O(1) | Remove tail of list |
MoveToEnd | O(1) | Relink nodes |
GetOrSet | O(1) | Atomic get-or-create |
Len | O(1) | Cached length |
Range | O(n) | Snapshot + iteration |
RangeLocked | O(n) | No allocation, holds lock |
Keys/Values | O(n) | Returns defensive copy |
Front/Back | O(1) | Direct pointer access |
At | O(n) | Must traverse list |
All operations are thread-safe via internal sync.RWMutex
:
- Reads (Get, Has, Len, Range, Keys, Values, Front, Back) use RLock
- Writes (Set, Delete, Clear, Reset, Pop*, MoveToEnd) use Lock
var om orderedmap.OrderedMap[int, string]
// Safe concurrent access from multiple goroutines
go func() { om.Set(1, "a") }()
go func() { om.Set(2, "b") }()
go func() { val, _ := om.Get(1) }()
Range and RangeBreak take snapshots before calling your callback, releasing the lock during iteration. This means:
✅ Safe - Callbacks can modify the map without deadlock:
om.Range(func(k string, v int) {
om.Set("new_"+k, v*2) // No deadlock!
om.Delete("old") // No deadlock!
})
❌ RangeLocked is NOT safe for re-entrant calls:
om.RangeLocked(func(k string, v int) {
om.Set("new", 1) // DEADLOCK!
})
// Prevent UI flickering in terminal applications
var checks orderedmap.OrderedMap[string, Status]
// Updates maintain order
checks.Set("database", StatusOK)
checks.Set("cache", StatusOK)
checks.Set("api", StatusFail)
// Always renders in same order (no flicker)
checks.Range(func(name string, status Status) {
fmt.Printf(" %s %s\n", statusIcon(status), name)
})
type LRUCache struct {
items orderedmap.OrderedMap[string, []byte]
maxSize int
}
func (c *LRUCache) Get(key string) ([]byte, bool) {
if val, ok := c.items.Get(key); ok {
c.items.MoveToEnd(key) // O(1) - move to back for LRU
return val, true
}
return nil, false
}
func (c *LRUCache) Set(key string, value []byte) {
c.items.Set(key, value)
c.items.MoveToEnd(key) // O(1) - newest at back
// Evict oldest if over capacity
for c.items.Len() > c.maxSize {
c.items.PopFront() // O(1) - remove oldest
}
}
// Process unique items in order received
var queue orderedmap.OrderedMap[string, Task]
queue.Set(task.ID, task) // Dedupe by ID
for {
id, task, ok := queue.PopFront() // O(1)
if !ok { break }
process(task)
}
type ConfigManager struct {
settings orderedmap.OrderedMap[string, string]
}
func (cm *ConfigManager) Load(file string) {
// Settings maintain file order for display
cm.settings.Set("timeout", "30s")
cm.settings.Set("retries", "3")
cm.settings.Set("endpoint", "https://api.example.com")
}
func (cm *ConfigManager) Display() {
cm.settings.Range(func(key, val string) {
fmt.Printf("%s = %s\n", key, val)
})
}
Feature | OrderedMap | map[K]V |
container/list |
---|---|---|---|
Insertion order | ✅ | ❌ (random) | ✅ |
O(1) lookup | ✅ | ✅ | ❌ (O(n)) |
O(1) delete | ✅ | ✅ | ✅ |
O(1) move | ✅ | ❌ | ✅ |
Thread-safe | ✅ | ❌ | ❌ |
Generic | ✅ | ✅ | ✅ (Go 1.18+) |
Zero value usable | ✅ | ✅ | ❌ |
- Go 1.18+ (uses generics)
# Run tests
go test -v
# Run tests with race detector
go test -v -race
# Check coverage
go test -v -coverprofile=coverage.txt
go tool cover -html=coverage.txt
# Run benchmarks
go test -bench=. -benchmem
Current test coverage: 100%
Contributions welcome! Please:
- Fork the repository
- Create a feature branch
- Add tests for new functionality
- Ensure 100% test coverage
- Run
go fmt
andgo vet
- Submit a pull request
MIT License - see LICENSE file for details
DocSpring, Inc. (@DocSpring)
Inspired by the need for stable UI rendering in terminal applications and the lack of a simple, thread-safe ordered map with O(1) operations in Go's standard library.