├── .gitignore ├── go.mod ├── go.sum ├── LICENSE ├── .travis.yml ├── README-zh.md ├── README.md ├── concurrent_map_bench_test.go ├── concurrent_map.go └── concurrent_map_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/workato/concurrent-map/v2 2 | 3 | go 1.18 4 | 5 | require github.com/mitchellh/hashstructure v1.1.0 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/mitchellh/hashstructure v1.1.0 h1:P6P1hdjqAAknpY/M1CGipelZgp+4y9ja9kmUZPXP+H0= 2 | github.com/mitchellh/hashstructure v1.1.0/go.mod h1:xUDAozZz0Wmdiufv0uyhnHkUTN6/6d8ulp4AwfLKrmA= 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 streamrail 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # This is a weird way of telling Travis to use the fast container-based test 2 | # runner instead of the slow VM-based runner. 3 | sudo: false 4 | 5 | language: go 6 | 7 | # You don't need to test on very old version of the Go compiler. It's the user's 8 | # responsibility to keep their compilers up to date. 9 | go: 10 | - 1.18 11 | 12 | # Only clone the most recent commit. 13 | git: 14 | depth: 1 15 | 16 | # Skip the install step. Don't `go get` dependencies. Only build with the code 17 | # in vendor/ 18 | install: true 19 | 20 | # Don't email me the results of the test runs. 21 | notifications: 22 | email: false 23 | 24 | before_script: 25 | - curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b $(go env GOPATH)/bin latest 26 | 27 | # script always runs to completion (set +e). If we have linter issues AND a 28 | # failing test, we want to see both. Configure golangci-lint with a 29 | # .golangci.yml file at the top level of your repo. 30 | script: 31 | - golangci-lint run # run a bunch of code checkers/linters in parallel 32 | - go test -v -race ./... # Run all the tests with the race detector enabled 33 | -------------------------------------------------------------------------------- /README-zh.md: -------------------------------------------------------------------------------- 1 | # concurrent map [![Build Status](https://travis-ci.com/orcaman/concurrent-map.svg?branch=master)](https://travis-ci.com/orcaman/concurrent-map) 2 | 3 | 正如 [这里](http://golang.org/doc/faq#atomic_maps) 和 [这里](http://blog.golang.org/go-maps-in-action)所描述的, Go语言原生的`map`类型并不支持并发读写。`concurrent-map`提供了一种高性能的解决方案:通过对内部`map`进行分片,降低锁粒度,从而达到最少的锁等待时间(锁冲突) 4 | 5 | 在Go 1.9之前,go语言标准库中并没有实现并发`map`。在Go 1.9中,引入了`sync.Map`。新的`sync.Map`与此`concurrent-map`有几个关键区别。标准库中的`sync.Map`是专为`append-only`场景设计的。因此,如果您想将`Map`用于一个类似内存数据库,那么使用我们的版本可能会受益。你可以在golang repo上读到更多,[这里](https://github.com/golang/go/issues/21035) and [这里](https://stackoverflow.com/questions/11063473/map-with-concurrent-access) 6 | ***译注:`sync.Map`在读多写少性能比较好,否则并发性能很差*** 7 | 8 | ## 用法 9 | 10 | 导入包: 11 | 12 | ```go 13 | import ( 14 | "github.com/orcaman/concurrent-map" 15 | ) 16 | 17 | ``` 18 | 19 | ```bash 20 | go get "github.com/orcaman/concurrent-map" 21 | ``` 22 | 23 | 现在包被导入到了`cmap`命名空间下 24 | ***译注:通常包的限定前缀(命名空间)是和目录名一致的,但是这个包有点典型😂,不一致!!!所以用的时候注意*** 25 | 26 | ## 示例 27 | 28 | ```go 29 | 30 | // 创建一个新的 map. 31 | m := cmap.New() 32 | 33 | // 设置变量m一个键为“foo”值为“bar”键值对 34 | m.Set("foo", "bar") 35 | 36 | // 从m中获取指定键值. 37 | if tmp, ok := m.Get("foo"); ok { 38 | bar := tmp.(string) 39 | } 40 | 41 | // 删除键为“foo”的项 42 | m.Remove("foo") 43 | 44 | ``` 45 | 46 | 更多使用示例请查看`concurrent_map_test.go`. 47 | 48 | 运行测试: 49 | 50 | ```bash 51 | go test "github.com/orcaman/concurrent-map" 52 | ``` 53 | 54 | ## 贡献说明 55 | 56 | 我们非常欢迎大家的贡献。如欲合并贡献,请遵循以下指引: 57 | - 新建一个issue,并且叙述为什么这么做(解决一个bug,增加一个功能,等等) 58 | - 根据核心团队对上述问题的反馈,提交一个PR,描述变更并链接到该问题。 59 | - 新代码必须具有测试覆盖率。 60 | - 如果代码是关于性能问题的,则必须在流程中包括基准测试(无论是在问题中还是在PR中)。 61 | - 一般来说,我们希望`concurrent-map`尽可能简单,且与原生的`map`有相似的操作。当你新建issue时请注意这一点。 62 | 63 | ## 许可证 64 | MIT (see [LICENSE](https://github.com/orcaman/concurrent-map/blob/master/LICENSE) file) 65 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # concurrent map [![Build Status](https://travis-ci.com/orcaman/concurrent-map.svg?branch=master)](https://travis-ci.com/orcaman/concurrent-map) 2 | 3 | As explained [here](http://golang.org/doc/faq#atomic_maps) and [here](http://blog.golang.org/go-maps-in-action), the `map` type in Go doesn't support concurrent reads and writes. `concurrent-map` provides a high-performance solution to this by sharding the map with minimal time spent waiting for locks. 4 | 5 | Prior to Go 1.9, there was no concurrent map implementation in the stdlib. In Go 1.9, `sync.Map` was introduced. The new `sync.Map` has a few key differences from this map. The stdlib `sync.Map` is designed for append-only scenarios. So if you want to use the map for something more like in-memory db, you might benefit from using our version. You can read more about it in the golang repo, for example [here](https://github.com/golang/go/issues/21035) and [here](https://stackoverflow.com/questions/11063473/map-with-concurrent-access) 6 | 7 | ## usage 8 | 9 | Import the package: 10 | 11 | ```go 12 | import ( 13 | "github.com/orcaman/concurrent-map" 14 | ) 15 | 16 | ``` 17 | 18 | ```bash 19 | go get "github.com/orcaman/concurrent-map" 20 | ``` 21 | 22 | The package is now imported under the "cmap" namespace. 23 | 24 | ## example 25 | 26 | ```go 27 | 28 | // Create a new map. 29 | m := cmap.New() 30 | 31 | // Sets item within map, sets "bar" under key "foo" 32 | m.Set("foo", "bar") 33 | 34 | // Retrieve item from map. 35 | if tmp, ok := m.Get("foo"); ok { 36 | bar := tmp.(string) 37 | } 38 | 39 | // Removes item under key "foo" 40 | m.Remove("foo") 41 | 42 | ``` 43 | 44 | For more examples have a look at concurrent_map_test.go. 45 | 46 | Running tests: 47 | 48 | ```bash 49 | go test "github.com/orcaman/concurrent-map" 50 | ``` 51 | 52 | ## guidelines for contributing 53 | 54 | Contributions are highly welcome. In order for a contribution to be merged, please follow these guidelines: 55 | - Open an issue and describe what you are after (fixing a bug, adding an enhancement, etc.). 56 | - According to the core team's feedback on the above mentioned issue, submit a pull request, describing the changes and linking to the issue. 57 | - New code must have test coverage. 58 | - If the code is about performance issues, you must include benchmarks in the process (either in the issue or in the PR). 59 | - In general, we would like to keep `concurrent-map` as simple as possible and as similar to the native `map`. Please keep this in mind when opening issues. 60 | 61 | ## language 62 | - [中文说明](./README-zh.md) 63 | 64 | ## license 65 | MIT (see [LICENSE](https://github.com/orcaman/concurrent-map/blob/master/LICENSE) file) 66 | -------------------------------------------------------------------------------- /concurrent_map_bench_test.go: -------------------------------------------------------------------------------- 1 | package cmap 2 | 3 | import ( 4 | "strconv" 5 | "sync" 6 | "testing" 7 | ) 8 | 9 | func BenchmarkItems(b *testing.B) { 10 | m := New[string, Animal]() 11 | 12 | // Insert 100 elements. 13 | for i := 0; i < 10000; i++ { 14 | m.Set(strconv.Itoa(i), Animal{strconv.Itoa(i)}) 15 | } 16 | for i := 0; i < b.N; i++ { 17 | m.Items() 18 | } 19 | } 20 | 21 | func BenchmarkMarshalJson(b *testing.B) { 22 | m := New[string, Animal]() 23 | 24 | // Insert 100 elements. 25 | for i := 0; i < 10000; i++ { 26 | m.Set(strconv.Itoa(i), Animal{strconv.Itoa(i)}) 27 | } 28 | for i := 0; i < b.N; i++ { 29 | _, err := m.MarshalJSON() 30 | if err != nil { 31 | b.FailNow() 32 | } 33 | } 34 | } 35 | 36 | func BenchmarkStrconv(b *testing.B) { 37 | for i := 0; i < b.N; i++ { 38 | strconv.Itoa(i) 39 | } 40 | } 41 | 42 | func BenchmarkSingleInsertAbsent(b *testing.B) { 43 | m := New[string, string]() 44 | b.ResetTimer() 45 | for i := 0; i < b.N; i++ { 46 | m.Set(strconv.Itoa(i), "value") 47 | } 48 | } 49 | 50 | func BenchmarkSingleInsertAbsentSyncMap(b *testing.B) { 51 | var m sync.Map 52 | b.ResetTimer() 53 | for i := 0; i < b.N; i++ { 54 | m.Store(strconv.Itoa(i), "value") 55 | } 56 | } 57 | 58 | func BenchmarkSingleInsertPresent(b *testing.B) { 59 | m := New[string, string]() 60 | m.Set("key", "value") 61 | b.ResetTimer() 62 | for i := 0; i < b.N; i++ { 63 | m.Set("key", "value") 64 | } 65 | } 66 | 67 | func BenchmarkSingleInsertPresentSyncMap(b *testing.B) { 68 | var m sync.Map 69 | m.Store("key", "value") 70 | b.ResetTimer() 71 | for i := 0; i < b.N; i++ { 72 | m.Store("key", "value") 73 | } 74 | } 75 | 76 | func benchmarkMultiInsertDifferent(b *testing.B) { 77 | m := New[string, string]() 78 | finished := make(chan struct{}, b.N) 79 | _, set := GetSet(m, finished) 80 | b.ResetTimer() 81 | for i := 0; i < b.N; i++ { 82 | go set(strconv.Itoa(i), "value") 83 | } 84 | for i := 0; i < b.N; i++ { 85 | <-finished 86 | } 87 | } 88 | 89 | func BenchmarkMultiInsertDifferentSyncMap(b *testing.B) { 90 | var m sync.Map 91 | finished := make(chan struct{}, b.N) 92 | _, set := GetSetSyncMap[string](&m, finished) 93 | 94 | b.ResetTimer() 95 | for i := 0; i < b.N; i++ { 96 | go set(strconv.Itoa(i), "value") 97 | } 98 | for i := 0; i < b.N; i++ { 99 | <-finished 100 | } 101 | } 102 | 103 | func BenchmarkMultiInsertDifferent_1_Shard(b *testing.B) { 104 | runWithShards(benchmarkMultiInsertDifferent, b, 1) 105 | } 106 | func BenchmarkMultiInsertDifferent_16_Shard(b *testing.B) { 107 | runWithShards(benchmarkMultiInsertDifferent, b, 16) 108 | } 109 | func BenchmarkMultiInsertDifferent_32_Shard(b *testing.B) { 110 | runWithShards(benchmarkMultiInsertDifferent, b, 32) 111 | } 112 | func BenchmarkMultiInsertDifferent_256_Shard(b *testing.B) { 113 | runWithShards(benchmarkMultiGetSetDifferent, b, 256) 114 | } 115 | 116 | func BenchmarkMultiInsertSame(b *testing.B) { 117 | m := New[string, string]() 118 | finished := make(chan struct{}, b.N) 119 | _, set := GetSet(m, finished) 120 | m.Set("key", "value") 121 | b.ResetTimer() 122 | for i := 0; i < b.N; i++ { 123 | go set("key", "value") 124 | } 125 | for i := 0; i < b.N; i++ { 126 | <-finished 127 | } 128 | } 129 | 130 | func BenchmarkMultiInsertSameSyncMap(b *testing.B) { 131 | var m sync.Map 132 | finished := make(chan struct{}, b.N) 133 | _, set := GetSetSyncMap[string](&m, finished) 134 | m.Store("key", "value") 135 | b.ResetTimer() 136 | for i := 0; i < b.N; i++ { 137 | go set("key", "value") 138 | } 139 | for i := 0; i < b.N; i++ { 140 | <-finished 141 | } 142 | } 143 | 144 | func BenchmarkMultiGetSame(b *testing.B) { 145 | m := New[string, string]() 146 | finished := make(chan struct{}, b.N) 147 | get, _ := GetSet(m, finished) 148 | m.Set("key", "value") 149 | b.ResetTimer() 150 | for i := 0; i < b.N; i++ { 151 | go get("key", "value") 152 | } 153 | for i := 0; i < b.N; i++ { 154 | <-finished 155 | } 156 | } 157 | 158 | func BenchmarkMultiGetSameSyncMap(b *testing.B) { 159 | var m sync.Map 160 | finished := make(chan struct{}, b.N) 161 | get, _ := GetSetSyncMap[string](&m, finished) 162 | m.Store("key", "value") 163 | b.ResetTimer() 164 | for i := 0; i < b.N; i++ { 165 | go get("key", "value") 166 | } 167 | for i := 0; i < b.N; i++ { 168 | <-finished 169 | } 170 | } 171 | 172 | func benchmarkMultiGetSetDifferent(b *testing.B) { 173 | m := New[string, string]() 174 | finished := make(chan struct{}, 2*b.N) 175 | get, set := GetSet(m, finished) 176 | m.Set("-1", "value") 177 | b.ResetTimer() 178 | for i := 0; i < b.N; i++ { 179 | go set(strconv.Itoa(i-1), "value") 180 | go get(strconv.Itoa(i), "value") 181 | } 182 | for i := 0; i < 2*b.N; i++ { 183 | <-finished 184 | } 185 | } 186 | 187 | func BenchmarkMultiGetSetDifferentSyncMap(b *testing.B) { 188 | var m sync.Map 189 | finished := make(chan struct{}, 2*b.N) 190 | get, set := GetSetSyncMap[string](&m, finished) 191 | m.Store("-1", "value") 192 | b.ResetTimer() 193 | for i := 0; i < b.N; i++ { 194 | go set(strconv.Itoa(i-1), "value") 195 | go get(strconv.Itoa(i), "value") 196 | } 197 | for i := 0; i < 2*b.N; i++ { 198 | <-finished 199 | } 200 | } 201 | 202 | func BenchmarkMultiGetSetDifferent_1_Shard(b *testing.B) { 203 | runWithShards(benchmarkMultiGetSetDifferent, b, 1) 204 | } 205 | func BenchmarkMultiGetSetDifferent_16_Shard(b *testing.B) { 206 | runWithShards(benchmarkMultiGetSetDifferent, b, 16) 207 | } 208 | func BenchmarkMultiGetSetDifferent_32_Shard(b *testing.B) { 209 | runWithShards(benchmarkMultiGetSetDifferent, b, 32) 210 | } 211 | func BenchmarkMultiGetSetDifferent_256_Shard(b *testing.B) { 212 | runWithShards(benchmarkMultiGetSetDifferent, b, 256) 213 | } 214 | 215 | func benchmarkMultiGetSetBlock(b *testing.B) { 216 | m := New[string, string]() 217 | finished := make(chan struct{}, 2*b.N) 218 | get, set := GetSet(m, finished) 219 | for i := 0; i < b.N; i++ { 220 | m.Set(strconv.Itoa(i%100), "value") 221 | } 222 | b.ResetTimer() 223 | for i := 0; i < b.N; i++ { 224 | go set(strconv.Itoa(i%100), "value") 225 | go get(strconv.Itoa(i%100), "value") 226 | } 227 | for i := 0; i < 2*b.N; i++ { 228 | <-finished 229 | } 230 | } 231 | 232 | func BenchmarkMultiGetSetBlockSyncMap(b *testing.B) { 233 | var m sync.Map 234 | finished := make(chan struct{}, 2*b.N) 235 | get, set := GetSetSyncMap[string](&m, finished) 236 | for i := 0; i < b.N; i++ { 237 | m.Store(strconv.Itoa(i%100), "value") 238 | } 239 | b.ResetTimer() 240 | for i := 0; i < b.N; i++ { 241 | go set(strconv.Itoa(i%100), "value") 242 | go get(strconv.Itoa(i%100), "value") 243 | } 244 | for i := 0; i < 2*b.N; i++ { 245 | <-finished 246 | } 247 | } 248 | 249 | func BenchmarkMultiGetSetBlock_1_Shard(b *testing.B) { 250 | runWithShards(benchmarkMultiGetSetBlock, b, 1) 251 | } 252 | func BenchmarkMultiGetSetBlock_16_Shard(b *testing.B) { 253 | runWithShards(benchmarkMultiGetSetBlock, b, 16) 254 | } 255 | func BenchmarkMultiGetSetBlock_32_Shard(b *testing.B) { 256 | runWithShards(benchmarkMultiGetSetBlock, b, 32) 257 | } 258 | func BenchmarkMultiGetSetBlock_256_Shard(b *testing.B) { 259 | runWithShards(benchmarkMultiGetSetBlock, b, 256) 260 | } 261 | 262 | func GetSet[K comparable, V any](m ConcurrentMap[K, V], finished chan struct{}) (set func(key K, value V), get func(key K, value V)) { 263 | return func(key K, value V) { 264 | for i := 0; i < 10; i++ { 265 | m.Get(key) 266 | } 267 | finished <- struct{}{} 268 | }, func(key K, value V) { 269 | for i := 0; i < 10; i++ { 270 | m.Set(key, value) 271 | } 272 | finished <- struct{}{} 273 | } 274 | } 275 | 276 | func GetSetSyncMap[V any](m *sync.Map, finished chan struct{}) (get func(key string, value V), set func(key string, value V)) { 277 | get = func(key string, value V) { 278 | for i := 0; i < 10; i++ { 279 | m.Load(key) 280 | } 281 | finished <- struct{}{} 282 | } 283 | set = func(key string, value V) { 284 | for i := 0; i < 10; i++ { 285 | m.Store(key, value) 286 | } 287 | finished <- struct{}{} 288 | } 289 | return 290 | } 291 | 292 | func runWithShards(bench func(b *testing.B), b *testing.B, shardsCount int) { 293 | oldShardsCount := SHARD_COUNT 294 | SHARD_COUNT = shardsCount 295 | bench(b) 296 | SHARD_COUNT = oldShardsCount 297 | } 298 | 299 | func BenchmarkKeys(b *testing.B) { 300 | m := New[string, Animal]() 301 | 302 | // Insert 100 elements. 303 | for i := 0; i < 10000; i++ { 304 | m.Set(strconv.Itoa(i), Animal{strconv.Itoa(i)}) 305 | } 306 | for i := 0; i < b.N; i++ { 307 | m.Keys() 308 | } 309 | } 310 | -------------------------------------------------------------------------------- /concurrent_map.go: -------------------------------------------------------------------------------- 1 | package cmap 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "sync" 7 | 8 | "github.com/mitchellh/hashstructure" 9 | ) 10 | 11 | var SHARD_COUNT = 32 12 | 13 | // A "thread" safe map of type string:Anything. 14 | // To avoid lock bottlenecks this map is dived to several (SHARD_COUNT) map shards. 15 | type ConcurrentMap[K comparable, V any] []*ConcurrentMapShared[K, V] 16 | 17 | // A "thread" safe string to anything map. 18 | type ConcurrentMapShared[K comparable, V any] struct { 19 | items map[K]V 20 | sync.RWMutex // Read Write mutex, guards access to internal map. 21 | } 22 | 23 | // Creates a new concurrent map. 24 | func New[K comparable, V any]() ConcurrentMap[K, V] { 25 | m := make(ConcurrentMap[K, V], SHARD_COUNT) 26 | for i := 0; i < SHARD_COUNT; i++ { 27 | m[i] = &ConcurrentMapShared[K, V]{items: make(map[K]V)} 28 | } 29 | return m 30 | } 31 | 32 | // GetShard returns shard under given key 33 | func (m ConcurrentMap[K, V]) GetShard(key K) *ConcurrentMapShared[K, V] { 34 | res, _ := hashstructure.Hash(key, nil) 35 | return m[uint(res)%uint(SHARD_COUNT)] 36 | } 37 | 38 | func (m ConcurrentMap[K, V]) MSet(data map[K]V) { 39 | for key, value := range data { 40 | shard := m.GetShard(key) 41 | shard.Lock() 42 | shard.items[key] = value 43 | shard.Unlock() 44 | } 45 | } 46 | 47 | // Sets the given value under the specified key. 48 | func (m ConcurrentMap[K, V]) Set(key K, value V) { 49 | // Get map shard. 50 | shard := m.GetShard(key) 51 | shard.Lock() 52 | shard.items[key] = value 53 | shard.Unlock() 54 | } 55 | 56 | // Callback to return new element to be inserted into the map 57 | // It is called while lock is held, therefore it MUST NOT 58 | // try to access other keys in same map, as it can lead to deadlock since 59 | // Go sync.RWLock is not reentrant 60 | type UpsertCb[V any] func(exist bool, valueInMap V, newValue V) V 61 | 62 | // Insert or Update - updates existing element or inserts a new one using UpsertCb 63 | func (m ConcurrentMap[K, V]) Upsert(key K, value V, cb UpsertCb[V]) (res V) { 64 | shard := m.GetShard(key) 65 | shard.Lock() 66 | v, ok := shard.items[key] 67 | res = cb(ok, v, value) 68 | shard.items[key] = res 69 | shard.Unlock() 70 | return res 71 | } 72 | 73 | // Sets the given value under the specified key if no value was associated with it. 74 | func (m ConcurrentMap[K, V]) SetIfAbsent(key K, value V) bool { 75 | // Get map shard. 76 | shard := m.GetShard(key) 77 | shard.Lock() 78 | _, ok := shard.items[key] 79 | if !ok { 80 | shard.items[key] = value 81 | } 82 | shard.Unlock() 83 | return !ok 84 | } 85 | 86 | // Get retrieves an element from map under given key. 87 | func (m ConcurrentMap[K, V]) Get(key K) (V, bool) { 88 | // Get shard 89 | shard := m.GetShard(key) 90 | shard.RLock() 91 | // Get item from shard. 92 | val, ok := shard.items[key] 93 | shard.RUnlock() 94 | return val, ok 95 | } 96 | 97 | // GetCb is a callback executed in a map.GetCb() call, while Lock is held. 98 | type GetCb[V any] func(v V, exist bool) (V, bool) 99 | 100 | // GetCb locks the shard containing the key, retrieves its current value and calls the callback with those params. 101 | // Returns the element from map under given key. 102 | func (m ConcurrentMap[K, V]) GetCb(key K, cb GetCb[V]) (V, bool) { 103 | // Get shard 104 | shard := m.GetShard(key) 105 | shard.RLock() 106 | // Get item from shard. 107 | val, ok := shard.items[key] 108 | val, ok = cb(val, ok) 109 | shard.RUnlock() 110 | return val, ok 111 | } 112 | 113 | // Count returns the number of elements within the map. 114 | func (m ConcurrentMap[K, V]) Count() int { 115 | count := 0 116 | for i := 0; i < SHARD_COUNT; i++ { 117 | shard := m[i] 118 | shard.RLock() 119 | count += len(shard.items) 120 | shard.RUnlock() 121 | } 122 | return count 123 | } 124 | 125 | // Looks up an item under specified key 126 | func (m ConcurrentMap[K, V]) Has(key K) bool { 127 | // Get shard 128 | shard := m.GetShard(key) 129 | shard.RLock() 130 | // See if element is within shard. 131 | _, ok := shard.items[key] 132 | shard.RUnlock() 133 | return ok 134 | } 135 | 136 | // Remove removes an element from the map. 137 | func (m ConcurrentMap[K, V]) Remove(key K) { 138 | // Try to get shard. 139 | shard := m.GetShard(key) 140 | shard.Lock() 141 | delete(shard.items, key) 142 | shard.Unlock() 143 | } 144 | 145 | // RemoveCb is a callback executed in a map.RemoveCb() call, while Lock is held 146 | // If returns true, the element will be removed from the map 147 | type RemoveCb[K, V any] func(key K, v V, exists bool) bool 148 | 149 | // RemoveCb locks the shard containing the key, retrieves its current value and calls the callback with those params 150 | // If callback returns true and element exists, it will remove it from the map 151 | // Returns the value returned by the callback (even if element was not present in the map) 152 | func (m ConcurrentMap[K, V]) RemoveCb(key K, cb RemoveCb[K, V]) bool { 153 | // Try to get shard. 154 | shard := m.GetShard(key) 155 | shard.Lock() 156 | v, ok := shard.items[key] 157 | remove := cb(key, v, ok) 158 | if remove && ok { 159 | delete(shard.items, key) 160 | } 161 | shard.Unlock() 162 | return remove 163 | } 164 | 165 | // Pop removes an element from the map and returns it 166 | func (m ConcurrentMap[K, V]) Pop(key K) (v V, exists bool) { 167 | // Try to get shard. 168 | shard := m.GetShard(key) 169 | shard.Lock() 170 | v, exists = shard.items[key] 171 | delete(shard.items, key) 172 | shard.Unlock() 173 | return v, exists 174 | } 175 | 176 | // IsEmpty checks if map is empty. 177 | func (m ConcurrentMap[K, V]) IsEmpty() bool { 178 | return m.Count() == 0 179 | } 180 | 181 | // Used by the Iter & IterBuffered functions to wrap two variables together over a channel, 182 | type Tuple[K comparable, V any] struct { 183 | Key K 184 | Val V 185 | } 186 | 187 | // Iter returns an iterator which could be used in a for range loop. 188 | // 189 | // Deprecated: using IterBuffered() will get a better performance 190 | func (m ConcurrentMap[K, V]) Iter() <-chan Tuple[K, V] { 191 | chans := snapshot(m) 192 | ch := make(chan Tuple[K, V]) 193 | go fanIn(chans, ch) 194 | return ch 195 | } 196 | 197 | // IterBuffered returns a buffered iterator which could be used in a for range loop. 198 | func (m ConcurrentMap[K, V]) IterBuffered() <-chan Tuple[K, V] { 199 | chans := snapshot(m) 200 | total := 0 201 | for _, c := range chans { 202 | total += cap(c) 203 | } 204 | ch := make(chan Tuple[K, V], total) 205 | go fanIn(chans, ch) 206 | return ch 207 | } 208 | 209 | // Clear removes all items from map. 210 | func (m ConcurrentMap[K, V]) Clear() { 211 | for item := range m.IterBuffered() { 212 | m.Remove(item.Key) 213 | } 214 | } 215 | 216 | // Returns a array of channels that contains elements in each shard, 217 | // which likely takes a snapshot of `m`. 218 | // It returns once the size of each buffered channel is determined, 219 | // before all the channels are populated using goroutines. 220 | func snapshot[K comparable, V any](m ConcurrentMap[K, V]) (chans []chan Tuple[K, V]) { 221 | //When you access map items before initializing. 222 | if len(m) == 0 { 223 | panic(`cmap.ConcurrentMap is not initialized. Should run New() before usage.`) 224 | } 225 | chans = make([]chan Tuple[K, V], SHARD_COUNT) 226 | wg := sync.WaitGroup{} 227 | wg.Add(SHARD_COUNT) 228 | // Foreach shard. 229 | for index, shard := range m { 230 | go func(index int, shard *ConcurrentMapShared[K, V]) { 231 | // Foreach key, value pair. 232 | shard.RLock() 233 | chans[index] = make(chan Tuple[K, V], len(shard.items)) 234 | wg.Done() 235 | for key, val := range shard.items { 236 | chans[index] <- Tuple[K, V]{key, val} 237 | } 238 | shard.RUnlock() 239 | close(chans[index]) 240 | }(index, shard) 241 | } 242 | wg.Wait() 243 | return chans 244 | } 245 | 246 | // fanIn reads elements from channels `chans` into channel `out` 247 | func fanIn[K comparable, V any](chans []chan Tuple[K, V], out chan Tuple[K, V]) { 248 | wg := sync.WaitGroup{} 249 | wg.Add(len(chans)) 250 | for _, ch := range chans { 251 | go func(ch chan Tuple[K, V]) { 252 | for t := range ch { 253 | out <- t 254 | } 255 | wg.Done() 256 | }(ch) 257 | } 258 | wg.Wait() 259 | close(out) 260 | } 261 | 262 | // Items returns all items as map[string]V 263 | func (m ConcurrentMap[K, V]) Items() map[K]V { 264 | tmp := make(map[K]V) 265 | 266 | // Insert items to temporary map. 267 | for item := range m.IterBuffered() { 268 | tmp[item.Key] = item.Val 269 | } 270 | 271 | return tmp 272 | } 273 | 274 | // Iterator callbacalled for every key,value found in 275 | // maps. RLock is held for all calls for a given shard 276 | // therefore callback sess consistent view of a shard, 277 | // but not across the shards 278 | type IterCb[K comparable, V any] func(key K, v V) 279 | 280 | // Callback based iterator, cheapest way to read 281 | // all elements in a map. 282 | func (m ConcurrentMap[K, V]) IterCb(fn IterCb[K, V]) { 283 | for idx := range m { 284 | shard := (m)[idx] 285 | shard.RLock() 286 | for key, value := range shard.items { 287 | fn(key, value) 288 | } 289 | shard.RUnlock() 290 | } 291 | } 292 | 293 | // Keys returns all keys as []string 294 | func (m ConcurrentMap[K, V]) Keys() []K { 295 | count := m.Count() 296 | ch := make(chan K, count) 297 | go func() { 298 | // Foreach shard. 299 | wg := sync.WaitGroup{} 300 | wg.Add(SHARD_COUNT) 301 | for _, shard := range m { 302 | go func(shard *ConcurrentMapShared[K, V]) { 303 | // Foreach key, value pair. 304 | shard.RLock() 305 | for key := range shard.items { 306 | ch <- key 307 | } 308 | shard.RUnlock() 309 | wg.Done() 310 | }(shard) 311 | } 312 | wg.Wait() 313 | close(ch) 314 | }() 315 | 316 | // Generate keys 317 | keys := make([]K, 0, count) 318 | for k := range ch { 319 | keys = append(keys, k) 320 | } 321 | return keys 322 | } 323 | 324 | // Reviles ConcurrentMap "private" variables to json marshal. 325 | func (m ConcurrentMap[K, V]) MarshalJSON() ([]byte, error) { 326 | // Create a temporary map, which will hold all item spread across shards. 327 | tmp := make(map[string]V) 328 | 329 | // Insert items to temporary map. 330 | for item := range m.IterBuffered() { 331 | tmp[fmt.Sprintf("%v", item.Key)] = item.Val 332 | } 333 | return json.Marshal(tmp) 334 | } 335 | 336 | // Concurrent map uses Interface{} as its value, therefor JSON Unmarshal 337 | // will probably won't know which to type to unmarshal into, in such case 338 | // we'll end up with a value of type map[string]V, In most cases this isn't 339 | // out value type, this is why we've decided to remove this functionality. 340 | 341 | // func (m *ConcurrentMap) UnmarshalJSON(b []byte) (err error) { 342 | // // Reverse process of Marshal. 343 | 344 | // tmp := make(map[string]V) 345 | 346 | // // Unmarshal into a single map. 347 | // if err := json.Unmarshal(b, &tmp); err != nil { 348 | // return nil 349 | // } 350 | 351 | // // foreach key,value pair in temporary map insert into our concurrent map. 352 | // for key, val := range tmp { 353 | // m.Set(key, val) 354 | // } 355 | // return nil 356 | // } 357 | -------------------------------------------------------------------------------- /concurrent_map_test.go: -------------------------------------------------------------------------------- 1 | package cmap 2 | 3 | import ( 4 | "encoding/json" 5 | "sort" 6 | "strconv" 7 | "testing" 8 | ) 9 | 10 | type Animal struct { 11 | name string 12 | } 13 | 14 | func TestMapCreation(t *testing.T) { 15 | m := New[string, string]() 16 | if m == nil { 17 | t.Error("map is null.") 18 | } 19 | 20 | if m.Count() != 0 { 21 | t.Error("new map should be empty.") 22 | } 23 | } 24 | 25 | func TestInsert(t *testing.T) { 26 | m := New[string, Animal]() 27 | elephant := Animal{"elephant"} 28 | monkey := Animal{"monkey"} 29 | 30 | m.Set("elephant", elephant) 31 | m.Set("monkey", monkey) 32 | 33 | if m.Count() != 2 { 34 | t.Error("map should contain exactly two elements.") 35 | } 36 | } 37 | 38 | func TestInsertAbsent(t *testing.T) { 39 | m := New[string, Animal]() 40 | elephant := Animal{"elephant"} 41 | monkey := Animal{"monkey"} 42 | 43 | m.SetIfAbsent("elephant", elephant) 44 | if ok := m.SetIfAbsent("elephant", monkey); ok { 45 | t.Error("map set a new value even the entry is already present") 46 | } 47 | } 48 | 49 | func TestGet(t *testing.T) { 50 | m := New[string, Animal]() 51 | 52 | // Get a missing element. 53 | val, ok := m.Get("Money") 54 | 55 | if ok == true { 56 | t.Error("ok should be false when item is missing from map.") 57 | } 58 | 59 | if (val != Animal{}) { 60 | t.Error("Missing values should return as null.") 61 | } 62 | 63 | elephant := Animal{"elephant"} 64 | m.Set("elephant", elephant) 65 | 66 | // Retrieve inserted element. 67 | elephant, ok = m.Get("elephant") 68 | if ok == false { 69 | t.Error("ok should be true for item stored within the map.") 70 | } 71 | 72 | if elephant.name != "elephant" { 73 | t.Error("item was modified.") 74 | } 75 | } 76 | 77 | func TestHas(t *testing.T) { 78 | m := New[string, Animal]() 79 | 80 | // Get a missing element. 81 | if m.Has("Money") == true { 82 | t.Error("element shouldn't exists") 83 | } 84 | 85 | elephant := Animal{"elephant"} 86 | m.Set("elephant", elephant) 87 | 88 | if m.Has("elephant") == false { 89 | t.Error("element exists, expecting Has to return True.") 90 | } 91 | } 92 | 93 | func TestRemove(t *testing.T) { 94 | m := New[string, Animal]() 95 | 96 | monkey := Animal{"monkey"} 97 | m.Set("monkey", monkey) 98 | 99 | m.Remove("monkey") 100 | 101 | if m.Count() != 0 { 102 | t.Error("Expecting count to be zero once item was removed.") 103 | } 104 | 105 | temp, ok := m.Get("monkey") 106 | 107 | if ok != false { 108 | t.Error("Expecting ok to be false for missing items.") 109 | } 110 | 111 | if (temp != Animal{}) { 112 | t.Error("Expecting item to be nil after its removal.") 113 | } 114 | 115 | // Remove a none existing element. 116 | m.Remove("noone") 117 | } 118 | 119 | func TestRemoveCb(t *testing.T) { 120 | m := New[string, Animal]() 121 | 122 | monkey := Animal{"monkey"} 123 | m.Set("monkey", monkey) 124 | elephant := Animal{"elephant"} 125 | m.Set("elephant", elephant) 126 | 127 | var ( 128 | mapKey string 129 | mapVal Animal 130 | wasFound bool 131 | ) 132 | cb := func(key string, val Animal, exists bool) bool { 133 | mapKey = key 134 | mapVal = val 135 | wasFound = exists 136 | 137 | return val.name == "monkey" 138 | } 139 | 140 | // Monkey should be removed 141 | result := m.RemoveCb("monkey", cb) 142 | if !result { 143 | t.Errorf("Result was not true") 144 | } 145 | 146 | if mapKey != "monkey" { 147 | t.Error("Wrong key was provided to the callback") 148 | } 149 | 150 | if mapVal != monkey { 151 | t.Errorf("Wrong value was provided to the value") 152 | } 153 | 154 | if !wasFound { 155 | t.Errorf("Key was not found") 156 | } 157 | 158 | if m.Has("monkey") { 159 | t.Errorf("Key was not removed") 160 | } 161 | 162 | // Elephant should not be removed 163 | result = m.RemoveCb("elephant", cb) 164 | if result { 165 | t.Errorf("Result was true") 166 | } 167 | 168 | if mapKey != "elephant" { 169 | t.Error("Wrong key was provided to the callback") 170 | } 171 | 172 | if mapVal != elephant { 173 | t.Errorf("Wrong value was provided to the value") 174 | } 175 | 176 | if !wasFound { 177 | t.Errorf("Key was not found") 178 | } 179 | 180 | if !m.Has("elephant") { 181 | t.Errorf("Key was removed") 182 | } 183 | 184 | // Unset key should remain unset 185 | result = m.RemoveCb("horse", cb) 186 | if result { 187 | t.Errorf("Result was true") 188 | } 189 | 190 | if mapKey != "horse" { 191 | t.Error("Wrong key was provided to the callback") 192 | } 193 | 194 | if (mapVal != Animal{}) { 195 | t.Errorf("Wrong value was provided to the value") 196 | } 197 | 198 | if wasFound { 199 | t.Errorf("Key was found") 200 | } 201 | 202 | if m.Has("horse") { 203 | t.Errorf("Key was created") 204 | } 205 | } 206 | 207 | func TestPop(t *testing.T) { 208 | m := New[string, Animal]() 209 | 210 | monkey := Animal{"monkey"} 211 | m.Set("monkey", monkey) 212 | 213 | v, exists := m.Pop("monkey") 214 | 215 | if !exists || v != monkey { 216 | t.Error("Pop didn't find a monkey.") 217 | } 218 | 219 | v2, exists2 := m.Pop("monkey") 220 | 221 | if exists2 || v2 == monkey { 222 | t.Error("Pop keeps finding monkey") 223 | } 224 | 225 | if m.Count() != 0 { 226 | t.Error("Expecting count to be zero once item was Pop'ed.") 227 | } 228 | 229 | temp, ok := m.Get("monkey") 230 | 231 | if ok != false { 232 | t.Error("Expecting ok to be false for missing items.") 233 | } 234 | 235 | if (temp != Animal{}) { 236 | t.Error("Expecting item to be nil after its removal.") 237 | } 238 | } 239 | 240 | func TestCount(t *testing.T) { 241 | m := New[string, Animal]() 242 | for i := 0; i < 100; i++ { 243 | m.Set(strconv.Itoa(i), Animal{strconv.Itoa(i)}) 244 | } 245 | 246 | if m.Count() != 100 { 247 | t.Error("Expecting 100 element within map.") 248 | } 249 | } 250 | 251 | func TestIsEmpty(t *testing.T) { 252 | m := New[string, Animal]() 253 | 254 | if m.IsEmpty() == false { 255 | t.Error("new map should be empty") 256 | } 257 | 258 | m.Set("elephant", Animal{"elephant"}) 259 | 260 | if m.IsEmpty() != false { 261 | t.Error("map shouldn't be empty.") 262 | } 263 | } 264 | 265 | func TestIterator(t *testing.T) { 266 | m := New[string, Animal]() 267 | 268 | // Insert 100 elements. 269 | for i := 0; i < 100; i++ { 270 | m.Set(strconv.Itoa(i), Animal{strconv.Itoa(i)}) 271 | } 272 | 273 | counter := 0 274 | // Iterate over elements. 275 | for item := range m.Iter() { 276 | val := item.Val 277 | 278 | if (val == Animal{}) { 279 | t.Error("Expecting an object.") 280 | } 281 | counter++ 282 | } 283 | 284 | if counter != 100 { 285 | t.Error("We should have counted 100 elements.") 286 | } 287 | } 288 | 289 | func TestBufferedIterator(t *testing.T) { 290 | m := New[string, Animal]() 291 | 292 | // Insert 100 elements. 293 | for i := 0; i < 100; i++ { 294 | m.Set(strconv.Itoa(i), Animal{strconv.Itoa(i)}) 295 | } 296 | 297 | counter := 0 298 | // Iterate over elements. 299 | for item := range m.IterBuffered() { 300 | val := item.Val 301 | 302 | if (val == Animal{}) { 303 | t.Error("Expecting an object.") 304 | } 305 | counter++ 306 | } 307 | 308 | if counter != 100 { 309 | t.Error("We should have counted 100 elements.") 310 | } 311 | } 312 | 313 | func TestClear(t *testing.T) { 314 | m := New[string, Animal]() 315 | 316 | // Insert 100 elements. 317 | for i := 0; i < 100; i++ { 318 | m.Set(strconv.Itoa(i), Animal{strconv.Itoa(i)}) 319 | } 320 | 321 | m.Clear() 322 | 323 | if m.Count() != 0 { 324 | t.Error("We should have 0 elements.") 325 | } 326 | } 327 | 328 | func TestIterCb(t *testing.T) { 329 | m := New[string, Animal]() 330 | 331 | // Insert 100 elements. 332 | for i := 0; i < 100; i++ { 333 | m.Set(strconv.Itoa(i), Animal{strconv.Itoa(i)}) 334 | } 335 | 336 | counter := 0 337 | // Iterate over elements. 338 | m.IterCb(func(key string, v Animal) { 339 | counter++ 340 | }) 341 | if counter != 100 { 342 | t.Error("We should have counted 100 elements.") 343 | } 344 | } 345 | 346 | func TestItems(t *testing.T) { 347 | m := New[string, Animal]() 348 | 349 | // Insert 100 elements. 350 | for i := 0; i < 100; i++ { 351 | m.Set(strconv.Itoa(i), Animal{strconv.Itoa(i)}) 352 | } 353 | 354 | items := m.Items() 355 | 356 | if len(items) != 100 { 357 | t.Error("We should have counted 100 elements.") 358 | } 359 | } 360 | 361 | func TestConcurrent(t *testing.T) { 362 | m := New[string, int]() 363 | ch := make(chan int) 364 | const iterations = 1000 365 | var a [iterations]int 366 | 367 | // Using go routines insert 1000 ints into our map. 368 | go func() { 369 | for i := 0; i < iterations/2; i++ { 370 | // Add item to map. 371 | m.Set(strconv.Itoa(i), i) 372 | 373 | // Retrieve item from map. 374 | val, _ := m.Get(strconv.Itoa(i)) 375 | 376 | // Write to channel inserted value. 377 | ch <- val 378 | } // Call go routine with current index. 379 | }() 380 | 381 | go func() { 382 | for i := iterations / 2; i < iterations; i++ { 383 | // Add item to map. 384 | m.Set(strconv.Itoa(i), i) 385 | 386 | // Retrieve item from map. 387 | val, _ := m.Get(strconv.Itoa(i)) 388 | 389 | // Write to channel inserted value. 390 | ch <- val 391 | } // Call go routine with current index. 392 | }() 393 | 394 | // Wait for all go routines to finish. 395 | counter := 0 396 | for elem := range ch { 397 | a[counter] = elem 398 | counter++ 399 | if counter == iterations { 400 | break 401 | } 402 | } 403 | 404 | // Sorts array, will make is simpler to verify all inserted values we're returned. 405 | sort.Ints(a[0:iterations]) 406 | 407 | // Make sure map contains 1000 elements. 408 | if m.Count() != iterations { 409 | t.Error("Expecting 1000 elements.") 410 | } 411 | 412 | // Make sure all inserted values we're fetched from map. 413 | for i := 0; i < iterations; i++ { 414 | if i != a[i] { 415 | t.Error("missing value", i) 416 | } 417 | } 418 | } 419 | 420 | func TestJsonMarshal(t *testing.T) { 421 | SHARD_COUNT = 2 422 | defer func() { 423 | SHARD_COUNT = 32 424 | }() 425 | expected := "{\"a\":1,\"b\":2}" 426 | m := New[string, int]() 427 | m.Set("a", 1) 428 | m.Set("b", 2) 429 | j, err := json.Marshal(m) 430 | if err != nil { 431 | t.Error(err) 432 | } 433 | 434 | if string(j) != expected { 435 | t.Error("json", string(j), "differ from expected", expected) 436 | return 437 | } 438 | } 439 | 440 | func TestKeys(t *testing.T) { 441 | m := New[string, Animal]() 442 | 443 | // Insert 100 elements. 444 | for i := 0; i < 100; i++ { 445 | m.Set(strconv.Itoa(i), Animal{strconv.Itoa(i)}) 446 | } 447 | 448 | keys := m.Keys() 449 | if len(keys) != 100 { 450 | t.Error("We should have counted 100 elements.") 451 | } 452 | } 453 | 454 | func TestMInsert(t *testing.T) { 455 | animals := map[string]Animal{ 456 | "elephant": {"elephant"}, 457 | "monkey": {"monkey"}, 458 | } 459 | m := New[string, Animal]() 460 | m.MSet(animals) 461 | 462 | if m.Count() != 2 { 463 | t.Error("map should contain exactly two elements.") 464 | } 465 | } 466 | 467 | func TestUpsert(t *testing.T) { 468 | dolphin := Animal{"dolphin"} 469 | whale := Animal{"whale"} 470 | tiger := Animal{"tiger"} 471 | lion := Animal{"lion"} 472 | 473 | cb := func(exists bool, valueInMap Animal, newValue Animal) Animal { 474 | if !exists { 475 | return newValue 476 | } 477 | valueInMap.name += newValue.name 478 | return valueInMap 479 | } 480 | 481 | m := New[string, Animal]() 482 | m.Set("marine", dolphin) 483 | m.Upsert("marine", whale, cb) 484 | m.Upsert("predator", tiger, cb) 485 | m.Upsert("predator", lion, cb) 486 | 487 | if m.Count() != 2 { 488 | t.Error("map should contain exactly two elements.") 489 | } 490 | 491 | marineAnimals, ok := m.Get("marine") 492 | if marineAnimals.name != "dolphinwhale" || !ok { 493 | t.Error("Set, then Upsert failed") 494 | } 495 | 496 | predators, ok := m.Get("predator") 497 | if !ok || predators.name != "tigerlion" { 498 | t.Error("Upsert, then Upsert failed") 499 | } 500 | } 501 | 502 | func TestKeysWhenRemoving(t *testing.T) { 503 | m := New[string, Animal]() 504 | 505 | // Insert 100 elements. 506 | Total := 100 507 | for i := 0; i < Total; i++ { 508 | m.Set(strconv.Itoa(i), Animal{strconv.Itoa(i)}) 509 | } 510 | 511 | // Remove 10 elements concurrently. 512 | Num := 10 513 | for i := 0; i < Num; i++ { 514 | go func(c *ConcurrentMap[string, Animal], n int) { 515 | c.Remove(strconv.Itoa(n)) 516 | }(&m, i) 517 | } 518 | keys := m.Keys() 519 | for _, k := range keys { 520 | if k == "" { 521 | t.Error("Empty keys returned") 522 | } 523 | } 524 | } 525 | 526 | // 527 | func TestUnDrainedIter(t *testing.T) { 528 | m := New[string, Animal]() 529 | // Insert 100 elements. 530 | Total := 100 531 | for i := 0; i < Total; i++ { 532 | m.Set(strconv.Itoa(i), Animal{strconv.Itoa(i)}) 533 | } 534 | counter := 0 535 | // Iterate over elements. 536 | ch := m.Iter() 537 | for item := range ch { 538 | val := item.Val 539 | 540 | if (val == Animal{}) { 541 | t.Error("Expecting an object.") 542 | } 543 | counter++ 544 | if counter == 42 { 545 | break 546 | } 547 | } 548 | for i := Total; i < 2*Total; i++ { 549 | m.Set(strconv.Itoa(i), Animal{strconv.Itoa(i)}) 550 | } 551 | for item := range ch { 552 | val := item.Val 553 | 554 | if (val == Animal{}) { 555 | t.Error("Expecting an object.") 556 | } 557 | counter++ 558 | } 559 | 560 | if counter != 100 { 561 | t.Error("We should have been right where we stopped") 562 | } 563 | 564 | counter = 0 565 | for item := range m.IterBuffered() { 566 | val := item.Val 567 | 568 | if (val == Animal{}) { 569 | t.Error("Expecting an object.") 570 | } 571 | counter++ 572 | } 573 | 574 | if counter != 200 { 575 | t.Error("We should have counted 200 elements.") 576 | } 577 | } 578 | 579 | func TestUnDrainedIterBuffered(t *testing.T) { 580 | m := New[string, Animal]() 581 | // Insert 100 elements. 582 | Total := 100 583 | for i := 0; i < Total; i++ { 584 | m.Set(strconv.Itoa(i), Animal{strconv.Itoa(i)}) 585 | } 586 | counter := 0 587 | // Iterate over elements. 588 | ch := m.IterBuffered() 589 | for item := range ch { 590 | val := item.Val 591 | 592 | if (val == Animal{}) { 593 | t.Error("Expecting an object.") 594 | } 595 | counter++ 596 | if counter == 42 { 597 | break 598 | } 599 | } 600 | for i := Total; i < 2*Total; i++ { 601 | m.Set(strconv.Itoa(i), Animal{strconv.Itoa(i)}) 602 | } 603 | for item := range ch { 604 | val := item.Val 605 | 606 | if (val == Animal{}) { 607 | t.Error("Expecting an object.") 608 | } 609 | counter++ 610 | } 611 | 612 | if counter != 100 { 613 | t.Error("We should have been right where we stopped") 614 | } 615 | 616 | counter = 0 617 | for item := range m.IterBuffered() { 618 | val := item.Val 619 | 620 | if (val == Animal{}) { 621 | t.Error("Expecting an object.") 622 | } 623 | counter++ 624 | } 625 | 626 | if counter != 200 { 627 | t.Error("We should have counted 200 elements.") 628 | } 629 | } 630 | --------------------------------------------------------------------------------