Skip to content

Commit 476e28a

Browse files
authored
feat: add All iterator method to Map interface (#43)
1 parent 753a480 commit 476e28a

File tree

6 files changed

+120
-9
lines changed

6 files changed

+120
-9
lines changed

.github/workflows/build.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ jobs:
1313
runs-on: ubuntu-latest
1414
strategy:
1515
matrix:
16-
go-version: [1.20.x, 1.23.x]
16+
go-version: [1.23.x, 1.25.x]
1717
steps:
1818
- name: Setup Go
1919
uses: actions/setup-go@v5
@@ -27,7 +27,7 @@ jobs:
2727
run: go test -race ./... -coverprofile=coverage.out -covermode=atomic
2828

2929
- name: Upload coverage to Codecov
30-
if: ${{ matrix.go-version == '1.20.x' }}
30+
if: ${{ matrix.go-version == '1.23.x' }}
3131
uses: codecov/codecov-action@v4
3232
with:
3333
token: ${{ secrets.CODECOV_TOKEN }}

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
module github.com/reugn/async
22

3-
go 1.20
3+
go 1.23.0

map.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package async
22

3+
import "iter"
4+
35
// A Map is an object that maps keys to values.
46
type Map[K comparable, V any] interface {
57

@@ -41,4 +43,8 @@ type Map[K comparable, V any] interface {
4143

4244
// Values returns a slice of the values contained in this map.
4345
Values() []*V
46+
47+
// All returns an iterator of all key-value pairs in this map.
48+
// The order of the pairs is not specified.
49+
All() iter.Seq2[K, *V]
4450
}

map_concurrent.go

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package async
22

33
import (
4+
"iter"
45
"sync"
56
"sync/atomic"
67
"time"
@@ -89,11 +90,10 @@ func (cm *ConcurrentMap[K, V]) IsEmpty() bool {
8990
// KeySet returns a slice of the keys contained in this map.
9091
func (cm *ConcurrentMap[K, V]) KeySet() []K {
9192
keys := make([]K, 0, cm.Size())
92-
rangeKeysFunc := func(key any, _ any) bool {
93+
cm.smap().Range(func(key any, _ any) bool {
9394
keys = append(keys, key.(K))
9495
return true
95-
}
96-
cm.smap().Range(rangeKeysFunc)
96+
})
9797
return keys
9898
}
9999

@@ -128,14 +128,23 @@ func (cm *ConcurrentMap[K, V]) Size() int {
128128
// Values returns a slice of the values contained in this map.
129129
func (cm *ConcurrentMap[K, V]) Values() []*V {
130130
values := make([]*V, 0, cm.Size())
131-
rangeValuesFunc := func(_ any, value any) bool {
131+
cm.smap().Range(func(_ any, value any) bool {
132132
values = append(values, value.(*V))
133133
return true
134-
}
135-
cm.smap().Range(rangeValuesFunc)
134+
})
136135
return values
137136
}
138137

138+
// All returns an iterator of all key-value pairs in this map.
139+
// The order of the pairs is not specified.
140+
func (cm *ConcurrentMap[K, V]) All() iter.Seq2[K, *V] {
141+
return func(yield func(K, *V) bool) {
142+
cm.smap().Range(func(key, value any) bool {
143+
return yield(key.(K), value.(*V))
144+
})
145+
}
146+
}
147+
139148
//nolint:staticcheck
140149
func (cm *ConcurrentMap[K, V]) smap() *sync.Map {
141150
for {

map_sharded.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package async
33
import (
44
"fmt"
55
"hash/fnv"
6+
"iter"
67
"sync"
78
)
89

@@ -130,6 +131,20 @@ func (sm *ShardedMap[K, V]) Values() []*V {
130131
return values
131132
}
132133

134+
// All returns an iterator of all key-value pairs in this map.
135+
// The order of the pairs is not specified.
136+
func (sm *ShardedMap[K, V]) All() iter.Seq2[K, *V] {
137+
return func(yield func(K, *V) bool) {
138+
for _, shard := range sm.shardMap {
139+
for key, value := range shard.All() {
140+
if !yield(key, value) {
141+
return
142+
}
143+
}
144+
}
145+
}
146+
}
147+
133148
// shard returns an underlying synchronized map for the key.
134149
func (sm *ShardedMap[K, V]) shard(key K) Map[K, V] {
135150
return sm.shardMap[sm.hashFunc(key)%sm.shards]
@@ -256,3 +271,17 @@ func (sync *SynchronizedMap[K, V]) Values() []*V {
256271
}
257272
return values
258273
}
274+
275+
// All returns an iterator of all key-value pairs in this map.
276+
// The order of the pairs is not specified.
277+
func (sync *SynchronizedMap[K, V]) All() iter.Seq2[K, *V] {
278+
return func(yield func(K, *V) bool) {
279+
sync.RLock()
280+
defer sync.RUnlock()
281+
for key, value := range sync.store {
282+
if !yield(key, value) {
283+
return
284+
}
285+
}
286+
}
287+
}

map_test.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package async
22

33
import (
4+
"maps"
45
"runtime"
56
"strconv"
67
"sync"
@@ -12,9 +13,12 @@ import (
1213
)
1314

1415
func TestMap_Clear(t *testing.T) {
16+
t.Parallel()
17+
1518
tests := prepareTestMaps()
1619
for _, tt := range tests {
1720
t.Run(tt.name, func(t *testing.T) {
21+
t.Parallel()
1822
tt.m.Clear()
1923
assert.Equal(t, tt.m.Size(), 0)
2024
tt.m.Put(1, util.Ptr("a"))
@@ -24,9 +28,12 @@ func TestMap_Clear(t *testing.T) {
2428
}
2529

2630
func TestMap_ComputeIfAbsent(t *testing.T) {
31+
t.Parallel()
32+
2733
tests := prepareTestMaps()
2834
for _, tt := range tests {
2935
t.Run(tt.name, func(t *testing.T) {
36+
t.Parallel()
3037
assert.Equal(
3138
t,
3239
tt.m.ComputeIfAbsent(4, func(_ int) *string { return util.Ptr("d") }),
@@ -44,39 +51,51 @@ func TestMap_ComputeIfAbsent(t *testing.T) {
4451
}
4552

4653
func TestMap_ContainsKey(t *testing.T) {
54+
t.Parallel()
55+
4756
tests := prepareTestMaps()
4857
for _, tt := range tests {
4958
t.Run(tt.name, func(t *testing.T) {
59+
t.Parallel()
5060
assert.Equal(t, tt.m.ContainsKey(3), true)
5161
assert.Equal(t, tt.m.ContainsKey(4), false)
5262
})
5363
}
5464
}
5565

5666
func TestMap_Get(t *testing.T) {
67+
t.Parallel()
68+
5769
tests := prepareTestMaps()
5870
for _, tt := range tests {
5971
t.Run(tt.name, func(t *testing.T) {
72+
t.Parallel()
6073
assert.Equal(t, tt.m.Get(1), util.Ptr("a"))
6174
assert.IsNil(t, tt.m.Get(4))
6275
})
6376
}
6477
}
6578

6679
func TestMap_GetOrDefault(t *testing.T) {
80+
t.Parallel()
81+
6782
tests := prepareTestMaps()
6883
for _, tt := range tests {
6984
t.Run(tt.name, func(t *testing.T) {
85+
t.Parallel()
7086
assert.Equal(t, tt.m.GetOrDefault(1, util.Ptr("e")), util.Ptr("a"))
7187
assert.Equal(t, tt.m.GetOrDefault(5, util.Ptr("e")), util.Ptr("e"))
7288
})
7389
}
7490
}
7591

7692
func TestMap_IsEmpty(t *testing.T) {
93+
t.Parallel()
94+
7795
tests := prepareTestMaps()
7896
for _, tt := range tests {
7997
t.Run(tt.name, func(t *testing.T) {
98+
t.Parallel()
8099
assert.Equal(t, tt.m.IsEmpty(), false)
81100
tt.m.Clear()
82101
assert.Equal(t, tt.m.IsEmpty(), true)
@@ -85,9 +104,12 @@ func TestMap_IsEmpty(t *testing.T) {
85104
}
86105

87106
func TestMap_KeySet(t *testing.T) {
107+
t.Parallel()
108+
88109
tests := prepareTestMaps()
89110
for _, tt := range tests {
90111
t.Run(tt.name, func(t *testing.T) {
112+
t.Parallel()
91113
assert.ElementsMatch(t, tt.m.KeySet(), []int{1, 2, 3})
92114
tt.m.Put(4, util.Ptr("d"))
93115
assert.ElementsMatch(t, tt.m.KeySet(), []int{1, 2, 3, 4})
@@ -96,9 +118,12 @@ func TestMap_KeySet(t *testing.T) {
96118
}
97119

98120
func TestMap_Put(t *testing.T) {
121+
t.Parallel()
122+
99123
tests := prepareTestMaps()
100124
for _, tt := range tests {
101125
t.Run(tt.name, func(t *testing.T) {
126+
t.Parallel()
102127
assert.Equal(t, tt.m.Size(), 3)
103128
tt.m.Put(4, util.Ptr("d"))
104129
assert.Equal(t, tt.m.Size(), 4)
@@ -111,9 +136,12 @@ func TestMap_Put(t *testing.T) {
111136
}
112137

113138
func TestMap_Remove(t *testing.T) {
139+
t.Parallel()
140+
114141
tests := prepareTestMaps()
115142
for _, tt := range tests {
116143
t.Run(tt.name, func(t *testing.T) {
144+
t.Parallel()
117145
assert.Equal(t, tt.m.Remove(3), util.Ptr("c"))
118146
assert.Equal(t, tt.m.Size(), 2)
119147
assert.IsNil(t, tt.m.Remove(5))
@@ -123,18 +151,24 @@ func TestMap_Remove(t *testing.T) {
123151
}
124152

125153
func TestMap_Size(t *testing.T) {
154+
t.Parallel()
155+
126156
tests := prepareTestMaps()
127157
for _, tt := range tests {
128158
t.Run(tt.name, func(t *testing.T) {
159+
t.Parallel()
129160
assert.Equal(t, tt.m.Size(), 3)
130161
})
131162
}
132163
}
133164

134165
func TestMap_Values(t *testing.T) {
166+
t.Parallel()
167+
135168
tests := prepareTestMaps()
136169
for _, tt := range tests {
137170
t.Run(tt.name, func(t *testing.T) {
171+
t.Parallel()
138172
assert.ElementsMatch(
139173
t,
140174
tt.m.Values(),
@@ -150,7 +184,38 @@ func TestMap_Values(t *testing.T) {
150184
}
151185
}
152186

187+
func TestMap_All(t *testing.T) {
188+
t.Parallel()
189+
190+
tests := prepareTestMaps()
191+
for _, tt := range tests {
192+
t.Run(tt.name, func(t *testing.T) {
193+
t.Parallel()
194+
// collect all key-value pairs from the iterator
195+
collected := maps.Collect(tt.m.All())
196+
197+
// verify we got all 3 expected pairs
198+
expected := map[int]*string{
199+
1: util.Ptr("a"),
200+
2: util.Ptr("b"),
201+
3: util.Ptr("c"),
202+
}
203+
assert.Equal(t, collected, expected)
204+
205+
// add a new entry and verify it appears in the iterator
206+
tt.m.Put(4, util.Ptr("d"))
207+
collected = maps.Collect(tt.m.All())
208+
209+
// verify we now have 4 pairs
210+
expected[4] = util.Ptr("d")
211+
assert.Equal(t, collected, expected)
212+
})
213+
}
214+
}
215+
153216
func TestShardedMap_ConstructorArguments(t *testing.T) {
217+
t.Parallel()
218+
154219
assert.PanicMsgContains(t, func() {
155220
NewShardedMap[int, string](0)
156221
}, "nonpositive shards")
@@ -167,6 +232,8 @@ func TestShardedMap_ConstructorArguments(t *testing.T) {
167232
}
168233

169234
func TestConcurrentMap_MemoryLeaks(t *testing.T) {
235+
t.Parallel()
236+
170237
var statsBefore runtime.MemStats
171238
runtime.ReadMemStats(&statsBefore)
172239

0 commit comments

Comments
 (0)