Skip to content

Commit c6549dd

Browse files
ndbroadbentclaude
andcommitted
Rewrite with doubly-linked list for O(1) operations
Architecture changes: - Replace slice-based implementation with map + doubly-linked list - All operations now O(1) (except At and Range which are O(n)) - Delete: O(n) → O(1) - PopFront: O(n) → O(1) - MoveToEnd: O(n) → O(1) Testing improvements: - Add comprehensive race tests for GetOrSet with long mk() - Add fuzz tests for random operation sequences - Add property tests for order preservation and list integrity - Add reentrancy tests for Range vs RangeLocked - Add big-N tests (10k entries) with allocation tracking - Add benchmarks for all operations with concurrency patterns - Maintain 100% test coverage Documentation: - Rewrite README from scratch for v1.0 - Document O(1) complexity for all operations - Add clear warnings for RangeLocked deadlock potential - Add clear warnings for GetOrSet holding write lock - Add LRU cache example showcasing O(1) MoveToEnd CI fixes: - Update Go versions to 1.22, 1.23, 1.24 - Update golangci-lint to v2 (from v1) - Update golangci-lint-action to v8 (from v4) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent 4659b3f commit c6549dd

File tree

5 files changed

+1372
-142
lines changed

5 files changed

+1372
-142
lines changed

.github/workflows/test.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ jobs:
1212
runs-on: ubuntu-latest
1313
strategy:
1414
matrix:
15-
go-version: ['1.21', '1.22', '1.23']
15+
go-version: ['1.22', '1.23', '1.24']
1616

1717
steps:
1818
- name: Checkout code
@@ -56,9 +56,9 @@ jobs:
5656
- name: Set up Go
5757
uses: actions/setup-go@v5
5858
with:
59-
go-version: '1.23'
59+
go-version: '1.24'
6060

6161
- name: Run golangci-lint
62-
uses: golangci/golangci-lint-action@v4
62+
uses: golangci/golangci-lint-action@v8
6363
with:
64-
version: latest
64+
version: v2

README.md

Lines changed: 115 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# orderedmap
22

3-
A thread-safe, generic ordered map for Go that maintains insertion order while providing O(1) lookups.
3+
A thread-safe, generic ordered map for Go that maintains insertion order while providing O(1) operations for lookups, deletes, and moves.
44

55
[![Go Reference](https://pkg.go.dev/badge/github.com/DocSpring/orderedmap.svg)](https://pkg.go.dev/github.com/DocSpring/orderedmap)
66
[![Go Report Card](https://goreportcard.com/badge/github.com/DocSpring/orderedmap)](https://goreportcard.com/report/github.com/DocSpring/orderedmap)
@@ -10,7 +10,7 @@ A thread-safe, generic ordered map for Go that maintains insertion order while p
1010
## Features
1111

1212
- **Insertion order preservation** - Iterates in the order items were added (unlike Go's built-in maps)
13-
- **O(1) lookups** - Fast key-based access using internal index
13+
- **O(1) operations** - Fast lookups, deletes, and moves using map + doubly-linked list
1414
- **Thread-safe** - All operations use internal locking (RWMutex)
1515
- **Zero-value usable** - No constructor required: `var om OrderedMap[K,V]` just works
1616
- **Generic** - Works with any comparable key type and any value type
@@ -68,6 +68,16 @@ Go's built-in `map[K]V` has **random iteration order** by design. This causes pr
6868
- **FIFO/insertion-order semantics** - Process items in the order they were added
6969
- **Deterministic testing** - Avoid flaky tests from random map iteration
7070

71+
## Implementation
72+
73+
OrderedMap uses a **map + doubly-linked list** hybrid approach:
74+
75+
- **map[K]*node** - O(1) lookups by key
76+
- **Doubly-linked list** - O(1) deletes and moves, preserves insertion order
77+
- **Cached length** - O(1) len() operation
78+
79+
This combination provides optimal performance for all operations except indexed access (At), which is O(n).
80+
7181
## API
7282

7383
### Creating
@@ -92,7 +102,7 @@ val, ok := om.Get("key")
92102
// Has - Check existence (O(1))
93103
if om.Has("key") { ... }
94104

95-
// Delete - Remove entry (O(n) - see notes)
105+
// Delete - Remove entry (O(1))
96106
deleted := om.Delete("key")
97107

98108
// Len - Get size (O(1))
@@ -116,16 +126,54 @@ om.RangeBreak(func(key string, value int) bool {
116126

117127
**Important:** Range and RangeBreak take a **snapshot** before iterating, so the callback can safely modify the map without causing deadlocks.
118128

129+
### Zero-Allocation Iteration
130+
131+
```go
132+
// RangeLocked - Iterate without allocating a snapshot
133+
om.RangeLocked(func(key string, value int) {
134+
// Process items
135+
})
136+
```
137+
138+
**⚠️ WARNING:** The callback in `RangeLocked` **MUST NOT** call any OrderedMap methods or deadlock will occur. Use `Range()` if you need to modify the map during iteration.
139+
119140
### Access First/Last
120141

121142
```go
122-
// Front - Get first entry
143+
// Front - Get first entry (O(1))
123144
key, val, ok := om.Front()
124145

125-
// Back - Get last entry
146+
// Back - Get last entry (O(1))
126147
key, val, ok := om.Back()
148+
149+
// At - Get entry at index (O(n))
150+
key, val, ok := om.At(5)
127151
```
128152

153+
### Queue Operations
154+
155+
```go
156+
// PopFront - Remove and return first entry (O(1))
157+
key, val, ok := om.PopFront()
158+
159+
// PopBack - Remove and return last entry (O(1))
160+
key, val, ok := om.PopBack()
161+
162+
// MoveToEnd - Move key to end of order (O(1))
163+
moved := om.MoveToEnd("key")
164+
```
165+
166+
### Cache Patterns
167+
168+
```go
169+
// GetOrSet - Atomic get-or-create (O(1))
170+
val, existed := om.GetOrSet("key", func() int {
171+
return expensiveComputation()
172+
})
173+
```
174+
175+
**⚠️ WARNING:** The `mk` function in `GetOrSet` is called **while holding the write lock**. Keep it fast and simple to avoid blocking other operations.
176+
129177
### Bulk Operations
130178

131179
```go
@@ -146,23 +194,26 @@ om.Reset()
146194

147195
| Operation | Complexity | Notes |
148196
|-----------|-----------|-------|
149-
| Set | O(1) | Amortized due to slice growth |
150-
| Get | O(1) | Hash map lookup |
151-
| Has | O(1) | Hash map lookup |
152-
| Delete | **O(n)** | Must shift elements and reindex |
153-
| Len | O(1) | Cached length |
154-
| Range | O(n) | Snapshot + iteration |
155-
| Keys/Values | O(n) | Returns defensive copy |
156-
| Front/Back | O(1) | Direct slice access |
157-
158-
**Delete is O(n)** because it preserves insertion order by shifting elements. If you need frequent deletes with high performance, consider using tombstones + periodic compaction instead.
197+
| Set | **O(1)** | Amortized due to map growth |
198+
| Get | **O(1)** | Hash map lookup |
199+
| Has | **O(1)** | Hash map lookup |
200+
| Delete | **O(1)** | Doubly-linked list removal |
201+
| PopFront | **O(1)** | Remove head of list |
202+
| PopBack | **O(1)** | Remove tail of list |
203+
| MoveToEnd | **O(1)** | Relink nodes |
204+
| Len | **O(1)** | Cached length |
205+
| Range | **O(n)** | Snapshot + iteration |
206+
| RangeLocked | **O(n)** | No allocation, holds lock |
207+
| Keys/Values | **O(n)** | Returns defensive copy |
208+
| Front/Back | **O(1)** | Direct pointer access |
209+
| At | **O(n)** | Must traverse list |
159210

160211
## Thread Safety
161212

162213
All operations are thread-safe via internal `sync.RWMutex`:
163214

164215
- **Reads** (Get, Has, Len, Range, Keys, Values, Front, Back) use RLock
165-
- **Writes** (Set, Delete, Clear, Reset) use Lock
216+
- **Writes** (Set, Delete, Clear, Reset, Pop*, MoveToEnd) use Lock
166217

167218
```go
168219
var om orderedmap.OrderedMap[int, string]
@@ -182,11 +233,20 @@ Range and RangeBreak take snapshots **before** calling your callback, releasing
182233
```go
183234
om.Range(func(k string, v int) {
184235
om.Set("new_"+k, v*2) // No deadlock!
236+
om.Delete("old") // No deadlock!
185237
})
186238
```
187239

188240
⚠️ **Snapshot semantics** - The callback sees the state at the time Range was called, not live updates
189241

242+
**RangeLocked is NOT safe** for re-entrant calls:
243+
244+
```go
245+
om.RangeLocked(func(k string, v int) {
246+
om.Set("new", 1) // DEADLOCK!
247+
})
248+
```
249+
190250
## Use Cases
191251

192252
### Terminal UI - Stable Rendering
@@ -206,16 +266,31 @@ checks.Range(func(name string, status Status) {
206266
})
207267
```
208268

209-
### Configuration - Preserve Order
269+
### LRU Cache
210270

211271
```go
212-
// Maintain order from config file
213-
type Config struct {
214-
Settings orderedmap.OrderedMap[string, string]
272+
type LRUCache struct {
273+
items orderedmap.OrderedMap[string, []byte]
274+
maxSize int
275+
}
276+
277+
func (c *LRUCache) Get(key string) ([]byte, bool) {
278+
if val, ok := c.items.Get(key); ok {
279+
c.items.MoveToEnd(key) // O(1) - move to back for LRU
280+
return val, true
281+
}
282+
return nil, false
215283
}
216284

217-
// YAML/JSON preserves order during unmarshal
218-
// Display or process in original order
285+
func (c *LRUCache) Set(key string, value []byte) {
286+
c.items.Set(key, value)
287+
c.items.MoveToEnd(key) // O(1) - newest at back
288+
289+
// Evict oldest if over capacity
290+
for c.items.Len() > c.maxSize {
291+
c.items.PopFront() // O(1) - remove oldest
292+
}
293+
}
219294
```
220295

221296
### FIFO Queue with Deduplication
@@ -226,27 +301,13 @@ var queue orderedmap.OrderedMap[string, Task]
226301

227302
queue.Set(task.ID, task) // Dedupe by ID
228303
for {
229-
id, task, ok := queue.Front()
304+
id, task, ok := queue.PopFront() // O(1)
230305
if !ok { break }
231306
process(task)
232-
queue.Delete(id)
233307
}
234308
```
235309

236-
## Comparison with Alternatives
237-
238-
| Feature | OrderedMap | `map[K]V` | `container/list` |
239-
|---------|-----------|-----------|------------------|
240-
| Insertion order || ❌ (random) ||
241-
| O(1) lookup ||| ❌ (O(n)) |
242-
| Thread-safe ||||
243-
| Generic ||| ✅ (Go 1.18+) |
244-
| Zero value usable ||||
245-
| O(1) delete | ❌ (O(n)) |||
246-
247-
## Examples
248-
249-
### Example 1: Configuration Manager
310+
### Configuration Manager
250311

251312
```go
252313
type ConfigManager struct {
@@ -267,50 +328,17 @@ func (cm *ConfigManager) Display() {
267328
}
268329
```
269330

270-
### Example 2: LRU-like Cache
271-
272-
```go
273-
type Cache struct {
274-
items orderedmap.OrderedMap[string, []byte]
275-
maxSize int
276-
}
277-
278-
func (c *Cache) Set(key string, value []byte) {
279-
c.items.Set(key, value)
280-
281-
// Evict oldest if over capacity
282-
for c.items.Len() > c.maxSize {
283-
oldest, _, _ := c.items.Front()
284-
c.items.Delete(oldest)
285-
}
286-
}
287-
```
288-
289-
### Example 3: Event Log
290-
291-
```go
292-
type EventLog struct {
293-
events orderedmap.OrderedMap[string, Event]
294-
}
295-
296-
func (el *EventLog) Record(event Event) {
297-
el.events.Set(event.ID, event)
298-
}
299-
300-
func (el *EventLog) GetRecent(n int) []Event {
301-
var recent []Event
302-
count := 0
303-
304-
// Iterate from oldest to newest
305-
el.events.RangeBreak(func(_ string, event Event) bool {
306-
recent = append(recent, event)
307-
count++
308-
return count < n
309-
})
331+
## Comparison with Alternatives
310332

311-
return recent
312-
}
313-
```
333+
| Feature | OrderedMap | `map[K]V` | `container/list` |
334+
|---------|-----------|-----------|------------------|
335+
| Insertion order || ❌ (random) ||
336+
| O(1) lookup ||| ❌ (O(n)) |
337+
| O(1) delete ||||
338+
| O(1) move ||||
339+
| Thread-safe ||||
340+
| Generic ||| ✅ (Go 1.18+) |
341+
| Zero value usable ||||
314342

315343
## Requirements
316344

@@ -326,8 +354,11 @@ go test -v
326354
go test -v -race
327355

328356
# Check coverage
329-
go test -v -coverprofile=coverage.out
330-
go tool cover -html=coverage.out
357+
go test -v -coverprofile=coverage.txt
358+
go tool cover -html=coverage.txt
359+
360+
# Run benchmarks
361+
go test -bench=. -benchmem
331362
```
332363

333364
Current test coverage: **100%**
@@ -353,4 +384,4 @@ DocSpring, Inc. ([@DocSpring](https://github.com/DocSpring))
353384

354385
## Acknowledgments
355386

356-
Inspired by the need for stable UI rendering in terminal applications and the lack of a simple, thread-safe ordered map in Go's standard library.
387+
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.

0 commit comments

Comments
 (0)