├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── benchmark ├── go.mod ├── go.sum ├── map_test.go └── result.txt ├── go.mod ├── go.sum ├── safemap.go ├── safemap_bench_test.go └── safemap_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | -------------------------------------------------------------------------------- /.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 | - go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest 26 | - go get github.com/mattn/goveralls 27 | 28 | # script always runs to completion (set +e). If we have linter issues AND a 29 | # failing test, we want to see both. Configure golangci-lint with a 30 | # .golangci.yml file at the top level of your repo. 31 | script: 32 | - golangci-lint run # run a bunch of code checkers/linters in parallel 33 | - go test -v -race ./... # Run all the tests with the race detector enabled 34 | - goveralls -service=travis-ci 35 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # safemap [![Build Status](https://travis-ci.com/smallnest/safemap.svg?branch=master)](https://travis-ci.com/smallnest/safemap) 2 | 3 | > 一个线程安全的,支持泛型的,高性能的map库。基于`orcaman/concurrent-map`的实现和`github.com/dolthub/maphash`的hash算法。 4 | 5 | ## usage 6 | 7 | Import the package: 8 | 9 | ```go 10 | import ( 11 | "github.com/smallnest/safemap" 12 | ) 13 | 14 | ``` 15 | 16 | ```bash 17 | go get github.com/smallnest/safemap@latest 18 | ``` 19 | 20 | The package is now imported under the "safemap" namespace. 21 | 22 | ## example 23 | 24 | ```go 25 | 26 | // Create a new map (K type is string, and V type is string too). 27 | m := safemap.New[string,string]() 28 | 29 | // Sets item within map, sets "bar" under key "foo" 30 | m.Set("foo", "bar") 31 | 32 | // Retrieve item from map. 33 | bar, ok := m.Get("foo") 34 | 35 | // Removes item under key "foo" 36 | m.Remove("foo") 37 | 38 | // Create a new map (K type is int, and V type is string too). 39 | m2 := safemap.New[int,string]() 40 | 41 | // Sets item within map, sets "bar" under key 1 42 | m2.Set(1, "bar") 43 | 44 | // Retrieve item from map. 45 | bar, ok = m2.Get(1) 46 | 47 | // Removes item under key 1 48 | m2.Remove(1) 49 | 50 | ``` 51 | 52 | For more examples have a look at safemap_test.go. 53 | 54 | Running tests: 55 | 56 | ```bash 57 | go test github.com/smallnest/safemap 58 | ``` 59 | 60 | 61 | ## license 62 | MIT (see [LICENSE](https://github.com/orcaman/concurrent-map/blob/master/LICENSE) file) 63 | -------------------------------------------------------------------------------- /benchmark/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/smallnest/safemap/benchmark 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/alphadose/haxmap v1.2.0 7 | github.com/cornelk/hashmap v1.0.8 8 | github.com/puzpuzpuz/xsync/v2 v2.4.0 9 | github.com/smallnest/safemap v0.0.0-20220904104306-ea4af14ec8ea 10 | ) 11 | 12 | require ( 13 | github.com/dolthub/maphash v0.0.0-20221220182448-74e1e1ea1577 // indirect 14 | github.com/orcaman/concurrent-map v1.0.0 // indirect 15 | github.com/orcaman/concurrent-map/v2 v2.0.1 // indirect 16 | golang.org/x/exp v0.0.0-20221031165847-c99f073a8326 // indirect 17 | ) 18 | 19 | replace github.com/smallnest/safemap => ./.. 20 | -------------------------------------------------------------------------------- /benchmark/go.sum: -------------------------------------------------------------------------------- 1 | github.com/alphadose/haxmap v1.2.0 h1:noGrAmCE+gNheZ4KpW+sYj9W5uMcO1UAjbAq9XBOAfM= 2 | github.com/alphadose/haxmap v1.2.0/go.mod h1:rjHw1IAqbxm0S3U5tD16GoKsiAd8FWx5BJ2IYqXwgmM= 3 | github.com/cornelk/hashmap v1.0.8 h1:nv0AWgw02n+iDcawr5It4CjQIAcdMMKRrs10HOJYlrc= 4 | github.com/cornelk/hashmap v1.0.8/go.mod h1:RfZb7JO3RviW/rT6emczVuC/oxpdz4UsSB2LJSclR1k= 5 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 6 | github.com/dolthub/maphash v0.0.0-20221220182448-74e1e1ea1577 h1:SegEguMxToBn045KRHLIUlF2/jR7Y2qD6fF+3tdOfvI= 7 | github.com/dolthub/maphash v0.0.0-20221220182448-74e1e1ea1577/go.mod h1:gkg4Ch4CdCDu5h6PMriVLawB7koZ+5ijb9puGMV50a4= 8 | github.com/orcaman/concurrent-map v1.0.0 h1:I/2A2XPCb4IuQWcQhBhSwGfiuybl/J0ev9HDbW65HOY= 9 | github.com/orcaman/concurrent-map v1.0.0/go.mod h1:Lu3tH6HLW3feq74c2GC+jIMS/K2CFcDWnWD9XkenwhI= 10 | github.com/orcaman/concurrent-map/v2 v2.0.1 h1:jOJ5Pg2w1oeB6PeDurIYf6k9PQ+aTITr/6lP/L/zp6c= 11 | github.com/orcaman/concurrent-map/v2 v2.0.1/go.mod h1:9Eq3TG2oBe5FirmYWQfYO5iH1q0Jv47PLaNK++uCdOM= 12 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 13 | github.com/puzpuzpuz/xsync/v2 v2.4.0 h1:5sXAMHrtx1bg9nbRZTOn8T4MkWe5V+o8yKRH02Eznag= 14 | github.com/puzpuzpuz/xsync/v2 v2.4.0/go.mod h1:gD2H2krq/w52MfPLE+Uy64TzJDVY7lP2znR9qmR35kU= 15 | github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= 16 | golang.org/x/exp v0.0.0-20221031165847-c99f073a8326 h1:QfTh0HpN6hlw6D3vu8DAwC8pBIwikq0AI1evdm+FksE= 17 | golang.org/x/exp v0.0.0-20221031165847-c99f073a8326/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= 18 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 19 | -------------------------------------------------------------------------------- /benchmark/map_test.go: -------------------------------------------------------------------------------- 1 | package benchmark 2 | 3 | import ( 4 | "sync" 5 | "sync/atomic" 6 | "testing" 7 | 8 | "github.com/alphadose/haxmap" 9 | "github.com/cornelk/hashmap" 10 | "github.com/puzpuzpuz/xsync/v2" 11 | "github.com/smallnest/safemap" 12 | ) 13 | 14 | const ( 15 | epochs uintptr = 1 << 12 16 | mapSize = 8 17 | ) 18 | 19 | func setupHaxMap() *haxmap.Map[uintptr, uintptr] { 20 | m := haxmap.New[uintptr, uintptr](mapSize) 21 | for i := uintptr(0); i < epochs; i++ { 22 | m.Set(i, i) 23 | } 24 | return m 25 | } 26 | 27 | func setupGoSyncMap() *sync.Map { 28 | m := &sync.Map{} 29 | for i := uintptr(0); i < epochs; i++ { 30 | m.Store(i, i) 31 | } 32 | return m 33 | } 34 | 35 | func setupCornelkMap() *hashmap.Map[uintptr, uintptr] { 36 | m := hashmap.NewSized[uintptr, uintptr](mapSize) 37 | for i := uintptr(0); i < epochs; i++ { 38 | m.Set(i, i) 39 | } 40 | return m 41 | } 42 | 43 | func setupXsyncMap() *xsync.MapOf[uintptr, uintptr] { 44 | m := xsync.NewIntegerMapOf[uintptr, uintptr]() 45 | for i := uintptr(0); i < epochs; i++ { 46 | m.Store(i, i) 47 | } 48 | return m 49 | } 50 | 51 | func setupSafeMap() *safemap.SafeMap[uintptr, uintptr] { 52 | m := safemap.New[uintptr, uintptr]() 53 | for i := uintptr(0); i < epochs; i++ { 54 | m.Set(i, i) 55 | } 56 | return m 57 | } 58 | 59 | func BenchmarkHaxMapReadsOnly(b *testing.B) { 60 | m := setupHaxMap() 61 | b.ResetTimer() 62 | b.RunParallel(func(pb *testing.PB) { 63 | for pb.Next() { 64 | for i := uintptr(0); i < epochs; i++ { 65 | j, _ := m.Get(i) 66 | if j != i { 67 | b.Fail() 68 | } 69 | } 70 | } 71 | }) 72 | } 73 | 74 | func BenchmarkHaxMapReadsWithWrites(b *testing.B) { 75 | m := setupHaxMap() 76 | var writer uint32 77 | b.ResetTimer() 78 | b.RunParallel(func(pb *testing.PB) { 79 | // use 1 thread as writer 80 | if atomic.AddUint32(&writer, 1)%2 == 0 { 81 | for pb.Next() { 82 | for i := uintptr(0); i < epochs; i++ { 83 | m.Set(i, i) 84 | } 85 | } 86 | } else { 87 | for pb.Next() { 88 | for i := uintptr(0); i < epochs; i++ { 89 | j, _ := m.Get(i) 90 | if j != i { 91 | b.Fail() 92 | } 93 | } 94 | } 95 | } 96 | }) 97 | } 98 | 99 | func BenchmarkGoSyncMapReadsOnly(b *testing.B) { 100 | m := setupGoSyncMap() 101 | b.ResetTimer() 102 | b.RunParallel(func(pb *testing.PB) { 103 | for pb.Next() { 104 | for i := uintptr(0); i < epochs; i++ { 105 | j, _ := m.Load(i) 106 | if j != i { 107 | b.Fail() 108 | } 109 | } 110 | } 111 | }) 112 | } 113 | 114 | func BenchmarkGoSyncMapReadsWithWrites(b *testing.B) { 115 | m := setupGoSyncMap() 116 | var writer uint32 117 | b.ResetTimer() 118 | b.RunParallel(func(pb *testing.PB) { 119 | // use 1 thread as writer 120 | if atomic.AddUint32(&writer, 1)%2 == 0 { 121 | for pb.Next() { 122 | for i := uintptr(0); i < epochs; i++ { 123 | m.Store(i, i) 124 | } 125 | } 126 | } else { 127 | for pb.Next() { 128 | for i := uintptr(0); i < epochs; i++ { 129 | j, _ := m.Load(i) 130 | if j != i { 131 | b.Fail() 132 | } 133 | } 134 | } 135 | } 136 | }) 137 | } 138 | 139 | func BenchmarkCornelkMapReadsOnly(b *testing.B) { 140 | m := setupCornelkMap() 141 | b.ResetTimer() 142 | b.RunParallel(func(pb *testing.PB) { 143 | for pb.Next() { 144 | for i := uintptr(0); i < epochs; i++ { 145 | j, _ := m.Get(i) 146 | if j != i { 147 | b.Fail() 148 | } 149 | } 150 | } 151 | }) 152 | } 153 | 154 | func BenchmarkCornelkMapReadsWithWrites(b *testing.B) { 155 | m := setupCornelkMap() 156 | var writer uint32 157 | b.ResetTimer() 158 | b.RunParallel(func(pb *testing.PB) { 159 | // use 1 thread as writer 160 | if atomic.AddUint32(&writer, 1)%2 == 0 { 161 | for pb.Next() { 162 | for i := uintptr(0); i < epochs; i++ { 163 | m.Set(i, i) 164 | } 165 | } 166 | } else { 167 | for pb.Next() { 168 | for i := uintptr(0); i < epochs; i++ { 169 | j, _ := m.Get(i) 170 | if j != i { 171 | b.Fail() 172 | } 173 | } 174 | } 175 | } 176 | }) 177 | } 178 | 179 | func BenchmarkXsyncMapReadsOnly(b *testing.B) { 180 | m := setupXsyncMap() 181 | b.ResetTimer() 182 | b.RunParallel(func(pb *testing.PB) { 183 | for pb.Next() { 184 | for i := uintptr(0); i < epochs; i++ { 185 | j, _ := m.Load(i) 186 | if j != i { 187 | b.Fail() 188 | } 189 | } 190 | } 191 | }) 192 | } 193 | 194 | func BenchmarkXsyncMapReadsWithWrites(b *testing.B) { 195 | m := setupXsyncMap() 196 | var writer uint32 197 | b.ResetTimer() 198 | b.RunParallel(func(pb *testing.PB) { 199 | // use 1 thread as writer 200 | if atomic.AddUint32(&writer, 1)%2 == 0 { 201 | for pb.Next() { 202 | for i := uintptr(0); i < epochs; i++ { 203 | m.Store(i, i) 204 | } 205 | } 206 | } else { 207 | for pb.Next() { 208 | for i := uintptr(0); i < epochs; i++ { 209 | j, _ := m.Load(i) 210 | if j != i { 211 | b.Fail() 212 | } 213 | } 214 | } 215 | } 216 | }) 217 | } 218 | 219 | func BenchmarkSafeMapReadsOnly(b *testing.B) { 220 | m := setupSafeMap() 221 | b.ResetTimer() 222 | b.RunParallel(func(pb *testing.PB) { 223 | for pb.Next() { 224 | for i := uintptr(0); i < epochs; i++ { 225 | j, _ := m.Get(i) 226 | if j != i { 227 | b.Fail() 228 | } 229 | } 230 | } 231 | }) 232 | } 233 | 234 | func BenchmarkSafeMapReadsWithWrites(b *testing.B) { 235 | m := setupSafeMap() 236 | var writer uint32 237 | b.ResetTimer() 238 | b.RunParallel(func(pb *testing.PB) { 239 | // use 1 thread as writer 240 | if atomic.AddUint32(&writer, 1)%2 == 0 { 241 | for pb.Next() { 242 | for i := uintptr(0); i < epochs; i++ { 243 | m.Set(i, i) 244 | } 245 | } 246 | } else { 247 | for pb.Next() { 248 | for i := uintptr(0); i < epochs; i++ { 249 | j, _ := m.Get(i) 250 | if j != i { 251 | b.Fail() 252 | } 253 | } 254 | } 255 | } 256 | }) 257 | } 258 | 259 | func BenchmarkMutexMapReadsOnly(b *testing.B) { 260 | m := setupSafeMap() 261 | var mutex sync.RWMutex 262 | b.ResetTimer() 263 | b.RunParallel(func(pb *testing.PB) { 264 | for pb.Next() { 265 | for i := uintptr(0); i < epochs; i++ { 266 | mutex.RLock() 267 | j, _ := m.Get(i) 268 | mutex.RUnlock() 269 | if j != i { 270 | b.Fail() 271 | } 272 | } 273 | } 274 | }) 275 | } 276 | 277 | func BenchmarkMutexMapReadsWithWrites(b *testing.B) { 278 | m := setupSafeMap() 279 | var mutex sync.RWMutex 280 | var writer uint32 281 | b.ResetTimer() 282 | b.RunParallel(func(pb *testing.PB) { 283 | // use 1 thread as writer 284 | if atomic.AddUint32(&writer, 1)%2 == 0 { 285 | for pb.Next() { 286 | for i := uintptr(0); i < epochs; i++ { 287 | mutex.Lock() 288 | m.Set(i, i) 289 | mutex.Unlock() 290 | } 291 | } 292 | } else { 293 | for pb.Next() { 294 | for i := uintptr(0); i < epochs; i++ { 295 | mutex.RLock() 296 | j, _ := m.Get(i) 297 | mutex.RUnlock() 298 | if j != i { 299 | b.Fail() 300 | } 301 | } 302 | } 303 | } 304 | }) 305 | } 306 | -------------------------------------------------------------------------------- /benchmark/result.txt: -------------------------------------------------------------------------------- 1 | BenchmarkHaxMapReadsOnly-8 113995 10511 ns/op 0 B/op 0 allocs/op 2 | BenchmarkHaxMapReadsWithWrites-8 65848 17755 ns/op 6575 B/op 821 allocs/op 3 | BenchmarkGoSyncMapReadsOnly-8 33531 32585 ns/op 0 B/op 0 allocs/op 4 | BenchmarkGoSyncMapReadsWithWrites-8 22123 52612 ns/op 28623 B/op 2654 allocs/op 5 | BenchmarkCornelkMapReadsOnly-8 105493 10128 ns/op 0 B/op 0 allocs/op 6 | BenchmarkCornelkMapReadsWithWrites-8 62604 19077 ns/op 7393 B/op 924 allocs/op 7 | BenchmarkXsyncMapReadsOnly-8 231136 4831 ns/op 0 B/op 0 allocs/op 8 | BenchmarkXsyncMapReadsWithWrites-8 160939 7321 ns/op 2837 B/op 177 allocs/op 9 | BenchmarkSafeMapReadsOnly-8 5928 197784 ns/op 0 B/op 0 allocs/op 10 | BenchmarkSafeMapReadsWithWrites-8 6751 183343 ns/op 0 B/op 0 allocs/op 11 | BenchmarkMutexMapReadsOnly-8 3021 389591 ns/op 0 B/op 0 allocs/op 12 | BenchmarkMutexMapReadsWithWrites-8 1742 807355 ns/op 3 B/op 0 allocs/op -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/smallnest/safemap 2 | 3 | go 1.18 4 | 5 | require github.com/dolthub/maphash v0.0.0-20221220182448-74e1e1ea1577 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 2 | github.com/dolthub/maphash v0.0.0-20221220182448-74e1e1ea1577 h1:SegEguMxToBn045KRHLIUlF2/jR7Y2qD6fF+3tdOfvI= 3 | github.com/dolthub/maphash v0.0.0-20221220182448-74e1e1ea1577/go.mod h1:gkg4Ch4CdCDu5h6PMriVLawB7koZ+5ijb9puGMV50a4= 4 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 5 | github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= 6 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 7 | -------------------------------------------------------------------------------- /safemap.go: -------------------------------------------------------------------------------- 1 | package safemap 2 | 3 | import ( 4 | "encoding/json" 5 | "runtime" 6 | "sync" 7 | 8 | "github.com/dolthub/maphash" 9 | ) 10 | 11 | var ShardCount = runtime.GOMAXPROCS(-1) 12 | 13 | // A "thread" safe map of type string:Anything. 14 | // To avoid lock bottlenecks this map is dived to several (ShardCount) map shards. 15 | type SafeMap[K comparable, V any] struct { 16 | shared []*SafeMapShared[K, V] 17 | hasher *maphash.Hasher[K] 18 | } 19 | 20 | // A "thread" safe string to anything map. 21 | type SafeMapShared[K comparable, V any] struct { 22 | items map[K]V 23 | sync.RWMutex // Read Write mutex, guards access to internal map. 24 | } 25 | 26 | // Creates a new concurrent map. 27 | func New[K comparable, V any]() *SafeMap[K, V] { 28 | shared := make([]*SafeMapShared[K, V], ShardCount) 29 | for i := 0; i < ShardCount; i++ { 30 | shared[i] = &SafeMapShared[K, V]{items: make(map[K]V)} 31 | } 32 | 33 | hasher := maphash.NewHasher[K]() 34 | return &SafeMap[K, V]{ 35 | shared: shared, 36 | hasher: &hasher, 37 | } 38 | } 39 | 40 | // GetShard returns shard under given key 41 | func (m *SafeMap[K, V]) GetShard(key K) *SafeMapShared[K, V] { 42 | k := uint(m.hasher.Hash(key)) 43 | return m.shared[uint(k)%uint(ShardCount)] 44 | } 45 | 46 | func (m *SafeMap[K, V]) MSet(data map[K]V) { 47 | for key, value := range data { 48 | shard := m.GetShard(key) 49 | shard.Lock() 50 | shard.items[key] = value 51 | shard.Unlock() 52 | } 53 | } 54 | 55 | // Sets the given value under the specified key. 56 | func (m *SafeMap[K, V]) Set(key K, value V) { 57 | // Get map shard. 58 | shard := m.GetShard(key) 59 | shard.Lock() 60 | shard.items[key] = value 61 | shard.Unlock() 62 | } 63 | 64 | // Callback to return new element to be inserted into the map 65 | // It is called while lock is held, therefore it MUST NOT 66 | // try to access other keys in same map, as it can lead to deadlock since 67 | // Go sync.RWLock is not reentrant 68 | type UpsertCb[K comparable, V any] func(exist bool, valueInMap V, newValue V) V 69 | 70 | // Insert or Update - updates existing element or inserts a new one using UpsertCb 71 | func (m *SafeMap[K, V]) Upsert(key K, value V, cb UpsertCb[K, V]) (res V) { 72 | shard := m.GetShard(key) 73 | shard.Lock() 74 | v, ok := shard.items[key] 75 | res = cb(ok, v, value) 76 | shard.items[key] = res 77 | shard.Unlock() 78 | return res 79 | } 80 | 81 | // Sets the given value under the specified key if no value was associated with it. 82 | func (m *SafeMap[K, V]) SetIfAbsent(key K, value V) bool { 83 | // Get map shard. 84 | shard := m.GetShard(key) 85 | shard.Lock() 86 | _, ok := shard.items[key] 87 | if !ok { 88 | shard.items[key] = value 89 | } 90 | shard.Unlock() 91 | return !ok 92 | } 93 | 94 | // Get retrieves an element from map under given key. 95 | func (m *SafeMap[K, V]) Get(key K) (V, bool) { 96 | // Get shard 97 | shard := m.GetShard(key) 98 | shard.RLock() 99 | // Get item from shard. 100 | val, ok := shard.items[key] 101 | shard.RUnlock() 102 | 103 | return val, ok 104 | } 105 | 106 | // Count returns the number of elements within the map. 107 | func (m *SafeMap[K, V]) Count() int { 108 | count := 0 109 | for i := 0; i < ShardCount; i++ { 110 | shard := m.shared[i] 111 | shard.RLock() 112 | count += len(shard.items) 113 | shard.RUnlock() 114 | } 115 | return count 116 | } 117 | 118 | // Looks up an item under specified key 119 | func (m *SafeMap[K, V]) Has(key K) bool { 120 | // Get shard 121 | shard := m.GetShard(key) 122 | shard.RLock() 123 | // See if element is within shard. 124 | _, ok := shard.items[key] 125 | shard.RUnlock() 126 | return ok 127 | } 128 | 129 | // Remove removes an element from the map. 130 | func (m *SafeMap[K, V]) Remove(key K) { 131 | // Try to get shard. 132 | shard := m.GetShard(key) 133 | shard.Lock() 134 | delete(shard.items, key) 135 | shard.Unlock() 136 | } 137 | 138 | // RemoveCb is a callback executed in a map.RemoveCb() call, while Lock is held 139 | // If returns true, the element will be removed from the map 140 | type RemoveCb[K comparable, V any] func(key K, v V, exists bool) bool 141 | 142 | // RemoveCb locks the shard containing the key, retrieves its current value and calls the callback with those params 143 | // If callback returns true and element exists, it will remove it from the map 144 | // Returns the value returned by the callback (even if element was not present in the map) 145 | func (m *SafeMap[K, V]) RemoveCb(key K, cb RemoveCb[K, V]) bool { 146 | // Try to get shard. 147 | shard := m.GetShard(key) 148 | shard.Lock() 149 | v, ok := shard.items[key] 150 | remove := cb(key, v, ok) 151 | if remove && ok { 152 | delete(shard.items, key) 153 | } 154 | shard.Unlock() 155 | return remove 156 | } 157 | 158 | // Pop removes an element from the map and returns it 159 | func (m *SafeMap[K, V]) Pop(key K) (v V, exists bool) { 160 | // Try to get shard. 161 | shard := m.GetShard(key) 162 | shard.Lock() 163 | v, exists = shard.items[key] 164 | delete(shard.items, key) 165 | shard.Unlock() 166 | return v, exists 167 | } 168 | 169 | // IsEmpty checks if map is empty. 170 | func (m *SafeMap[K, V]) IsEmpty() bool { 171 | return m.Count() == 0 172 | } 173 | 174 | // Used by the Iter & IterBuffered functions to wrap two variables together over a channel, 175 | type Tuple[K comparable, V any] struct { 176 | Key K 177 | Val V 178 | } 179 | 180 | // Iter returns an iterator which could be used in a for range loop. 181 | // 182 | // Deprecated: using IterBuffered() will get a better performence 183 | func (m *SafeMap[K, V]) Iter() <-chan Tuple[K, V] { 184 | chans := snapshot(m) 185 | ch := make(chan Tuple[K, V]) 186 | go fanIn(chans, ch) 187 | return ch 188 | } 189 | 190 | // IterBuffered returns a buffered iterator which could be used in a for range loop. 191 | func (m *SafeMap[K, V]) IterBuffered() <-chan Tuple[K, V] { 192 | chans := snapshot(m) 193 | total := 0 194 | for _, c := range chans { 195 | total += cap(c) 196 | } 197 | ch := make(chan Tuple[K, V], total) 198 | go fanIn(chans, ch) 199 | return ch 200 | } 201 | 202 | // Clear removes all items from map. 203 | func (m *SafeMap[K, V]) Clear() { 204 | for item := range m.IterBuffered() { 205 | m.Remove(item.Key) 206 | } 207 | } 208 | 209 | // Returns a array of channels that contains elements in each shard, 210 | // which likely takes a snapshot of `m`. 211 | // It returns once the size of each buffered channel is determined, 212 | // before all the channels are populated using goroutines. 213 | func snapshot[K comparable, V any](m *SafeMap[K, V]) (chans []chan Tuple[K, V]) { 214 | //When you access map items before initializing. 215 | if len(m.shared) == 0 { 216 | panic(`cmap.SafeMap is not initialized. Should run New() before usage.`) 217 | } 218 | chans = make([]chan Tuple[K, V], ShardCount) 219 | wg := sync.WaitGroup{} 220 | wg.Add(ShardCount) 221 | // Foreach shard. 222 | for index, shard := range m.shared { 223 | go func(index int, shard *SafeMapShared[K, V]) { 224 | // Foreach key, value pair. 225 | shard.RLock() 226 | chans[index] = make(chan Tuple[K, V], len(shard.items)) 227 | wg.Done() 228 | for key, val := range shard.items { 229 | chans[index] <- Tuple[K, V]{key, val} 230 | } 231 | shard.RUnlock() 232 | close(chans[index]) 233 | }(index, shard) 234 | } 235 | wg.Wait() 236 | return chans 237 | } 238 | 239 | // fanIn reads elements from channels `chans` into channel `out` 240 | func fanIn[K comparable, V any](chans []chan Tuple[K, V], out chan Tuple[K, V]) { 241 | wg := sync.WaitGroup{} 242 | wg.Add(len(chans)) 243 | for _, ch := range chans { 244 | go func(ch chan Tuple[K, V]) { 245 | for t := range ch { 246 | out <- t 247 | } 248 | wg.Done() 249 | }(ch) 250 | } 251 | wg.Wait() 252 | close(out) 253 | } 254 | 255 | // Items returns all items as map[string]V 256 | func (m *SafeMap[K, V]) Items() map[K]V { 257 | tmp := make(map[K]V) 258 | 259 | // Insert items to temporary map. 260 | for item := range m.IterBuffered() { 261 | tmp[item.Key] = item.Val 262 | } 263 | 264 | return tmp 265 | } 266 | 267 | // Iterator callbacalled for every key,value found in 268 | // maps. RLock is held for all calls for a given shard 269 | // therefore callback sess consistent view of a shard, 270 | // but not across the shards 271 | type IterCb[K comparable, V any] func(key K, v V) 272 | 273 | // Callback based iterator, cheapest way to read 274 | // all elements in a map. 275 | func (m *SafeMap[K, V]) IterCb(fn IterCb[K, V]) { 276 | shared := m.shared 277 | for idx := range shared { 278 | shard := shared[idx] 279 | shard.RLock() 280 | for key, value := range shard.items { 281 | fn(key, value) 282 | } 283 | shard.RUnlock() 284 | } 285 | } 286 | 287 | // Keys returns all keys as []string 288 | func (m *SafeMap[K, V]) Keys() []K { 289 | count := m.Count() 290 | ch := make(chan K, count) 291 | go func() { 292 | // Foreach shard. 293 | wg := sync.WaitGroup{} 294 | wg.Add(ShardCount) 295 | for _, shard := range m.shared { 296 | go func(shard *SafeMapShared[K, V]) { 297 | // Foreach key, value pair. 298 | shard.RLock() 299 | for key := range shard.items { 300 | ch <- key 301 | } 302 | shard.RUnlock() 303 | wg.Done() 304 | }(shard) 305 | } 306 | wg.Wait() 307 | close(ch) 308 | }() 309 | 310 | // Generate keys 311 | keys := make([]K, 0, count) 312 | for k := range ch { 313 | keys = append(keys, k) 314 | } 315 | return keys 316 | } 317 | 318 | // Reviles SafeMap "private" variables to json marshal. 319 | func (m *SafeMap[K, V]) MarshalJSON() ([]byte, error) { 320 | // Create a temporary map, which will hold all item spread across shards. 321 | tmp := make(map[K]V) 322 | 323 | // Insert items to temporary map. 324 | for item := range m.IterBuffered() { 325 | tmp[item.Key] = item.Val 326 | } 327 | return json.Marshal(tmp) 328 | } 329 | 330 | // Reverse process of Marshal. 331 | func (m *SafeMap[K, V]) UnmarshalJSON(b []byte) (err error) { 332 | tmp := make(map[K]V) 333 | 334 | // Unmarshal into a single map. 335 | if err := json.Unmarshal(b, &tmp); err != nil { 336 | return err 337 | } 338 | 339 | // foreach key,value pair in temporary map insert into our concurrent map. 340 | for key, val := range tmp { 341 | m.Set(key, val) 342 | } 343 | return nil 344 | } 345 | -------------------------------------------------------------------------------- /safemap_bench_test.go: -------------------------------------------------------------------------------- 1 | package safemap 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[string, string](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 *SafeMap[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 := ShardCount 294 | ShardCount = shardsCount 295 | bench(b) 296 | ShardCount = 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 | -------------------------------------------------------------------------------- /safemap_test.go: -------------------------------------------------------------------------------- 1 | package safemap 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "sort" 7 | "strconv" 8 | "testing" 9 | ) 10 | 11 | type Animal struct { 12 | name string 13 | } 14 | 15 | func TestMapCreation(t *testing.T) { 16 | m := New[string, string]() 17 | if m == nil { 18 | t.Error("map is null.") 19 | } 20 | 21 | if m.Count() != 0 { 22 | t.Error("new map should be empty.") 23 | } 24 | } 25 | 26 | func TestInsert(t *testing.T) { 27 | m := New[string, Animal]() 28 | elephant := Animal{"elephant"} 29 | monkey := Animal{"monkey"} 30 | 31 | m.Set("elephant", elephant) 32 | m.Set("monkey", monkey) 33 | 34 | if m.Count() != 2 { 35 | t.Error("map should contain exactly two elements.") 36 | } 37 | } 38 | 39 | func TestInsertAbsent(t *testing.T) { 40 | m := New[string, Animal]() 41 | elephant := Animal{"elephant"} 42 | monkey := Animal{"monkey"} 43 | 44 | m.SetIfAbsent("elephant", elephant) 45 | if ok := m.SetIfAbsent("elephant", monkey); ok { 46 | t.Error("map set a new value even the entry is already present") 47 | } 48 | } 49 | 50 | func TestGet(t *testing.T) { 51 | m := New[string, Animal]() 52 | 53 | // Get a missing element. 54 | val, ok := m.Get("Money") 55 | 56 | if ok == true { 57 | t.Error("ok should be false when item is missing from map.") 58 | } 59 | 60 | if (val != Animal{}) { 61 | t.Error("Missing values should return as null.") 62 | } 63 | 64 | elephant := Animal{"elephant"} 65 | m.Set("elephant", elephant) 66 | 67 | // Retrieve inserted element. 68 | elephant, ok = m.Get("elephant") 69 | if ok == false { 70 | t.Error("ok should be true for item stored within the map.") 71 | } 72 | 73 | if elephant.name != "elephant" { 74 | t.Error("item was modified.") 75 | } 76 | } 77 | 78 | func TestHas(t *testing.T) { 79 | m := New[string, Animal]() 80 | 81 | // Get a missing element. 82 | if m.Has("Money") == true { 83 | t.Error("element shouldn't exists") 84 | } 85 | 86 | elephant := Animal{"elephant"} 87 | m.Set("elephant", elephant) 88 | 89 | if m.Has("elephant") == false { 90 | t.Error("element exists, expecting Has to return True.") 91 | } 92 | } 93 | 94 | func TestRemove(t *testing.T) { 95 | m := New[string, Animal]() 96 | 97 | monkey := Animal{"monkey"} 98 | m.Set("monkey", monkey) 99 | 100 | m.Remove("monkey") 101 | 102 | if m.Count() != 0 { 103 | t.Error("Expecting count to be zero once item was removed.") 104 | } 105 | 106 | temp, ok := m.Get("monkey") 107 | 108 | if ok != false { 109 | t.Error("Expecting ok to be false for missing items.") 110 | } 111 | 112 | if (temp != Animal{}) { 113 | t.Error("Expecting item to be nil after its removal.") 114 | } 115 | 116 | // Remove a none existing element. 117 | m.Remove("noone") 118 | } 119 | 120 | func TestRemoveCb(t *testing.T) { 121 | m := New[string, Animal]() 122 | 123 | monkey := Animal{"monkey"} 124 | m.Set("monkey", monkey) 125 | elephant := Animal{"elephant"} 126 | m.Set("elephant", elephant) 127 | 128 | var ( 129 | mapKey string 130 | mapVal Animal 131 | wasFound bool 132 | ) 133 | cb := func(key string, val Animal, exists bool) bool { 134 | mapKey = key 135 | mapVal = val 136 | wasFound = exists 137 | 138 | return val.name == "monkey" 139 | } 140 | 141 | // Monkey should be removed 142 | result := m.RemoveCb("monkey", cb) 143 | if !result { 144 | t.Errorf("Result was not true") 145 | } 146 | 147 | if mapKey != "monkey" { 148 | t.Error("Wrong key was provided to the callback") 149 | } 150 | 151 | if mapVal != monkey { 152 | t.Errorf("Wrong value was provided to the value") 153 | } 154 | 155 | if !wasFound { 156 | t.Errorf("Key was not found") 157 | } 158 | 159 | if m.Has("monkey") { 160 | t.Errorf("Key was not removed") 161 | } 162 | 163 | // Elephant should not be removed 164 | result = m.RemoveCb("elephant", cb) 165 | if result { 166 | t.Errorf("Result was true") 167 | } 168 | 169 | if mapKey != "elephant" { 170 | t.Error("Wrong key was provided to the callback") 171 | } 172 | 173 | if mapVal != elephant { 174 | t.Errorf("Wrong value was provided to the value") 175 | } 176 | 177 | if !wasFound { 178 | t.Errorf("Key was not found") 179 | } 180 | 181 | if !m.Has("elephant") { 182 | t.Errorf("Key was removed") 183 | } 184 | 185 | // Unset key should remain unset 186 | result = m.RemoveCb("horse", cb) 187 | if result { 188 | t.Errorf("Result was true") 189 | } 190 | 191 | if mapKey != "horse" { 192 | t.Error("Wrong key was provided to the callback") 193 | } 194 | 195 | if (mapVal != Animal{}) { 196 | t.Errorf("Wrong value was provided to the value") 197 | } 198 | 199 | if wasFound { 200 | t.Errorf("Key was found") 201 | } 202 | 203 | if m.Has("horse") { 204 | t.Errorf("Key was created") 205 | } 206 | } 207 | 208 | func TestPop(t *testing.T) { 209 | m := New[string, Animal]() 210 | 211 | monkey := Animal{"monkey"} 212 | m.Set("monkey", monkey) 213 | 214 | v, exists := m.Pop("monkey") 215 | 216 | if !exists || v != monkey { 217 | t.Error("Pop didn't find a monkey.") 218 | } 219 | 220 | v2, exists2 := m.Pop("monkey") 221 | 222 | if exists2 || v2 == monkey { 223 | t.Error("Pop keeps finding monkey") 224 | } 225 | 226 | if m.Count() != 0 { 227 | t.Error("Expecting count to be zero once item was Pop'ed.") 228 | } 229 | 230 | temp, ok := m.Get("monkey") 231 | 232 | if ok != false { 233 | t.Error("Expecting ok to be false for missing items.") 234 | } 235 | 236 | if (temp != Animal{}) { 237 | t.Error("Expecting item to be nil after its removal.") 238 | } 239 | } 240 | 241 | func TestCount(t *testing.T) { 242 | m := New[string, Animal]() 243 | for i := 0; i < 100; i++ { 244 | m.Set(strconv.Itoa(i), Animal{strconv.Itoa(i)}) 245 | } 246 | 247 | if m.Count() != 100 { 248 | t.Error("Expecting 100 element within map.") 249 | } 250 | } 251 | 252 | func TestIsEmpty(t *testing.T) { 253 | m := New[string, Animal]() 254 | 255 | if m.IsEmpty() == false { 256 | t.Error("new map should be empty") 257 | } 258 | 259 | m.Set("elephant", Animal{"elephant"}) 260 | 261 | if m.IsEmpty() != false { 262 | t.Error("map shouldn't be empty.") 263 | } 264 | } 265 | 266 | func TestIterator(t *testing.T) { 267 | m := New[string, Animal]() 268 | 269 | // Insert 100 elements. 270 | for i := 0; i < 100; i++ { 271 | m.Set(strconv.Itoa(i), Animal{strconv.Itoa(i)}) 272 | } 273 | 274 | counter := 0 275 | // Iterate over elements. 276 | for item := range m.Iter() { 277 | val := item.Val 278 | 279 | if (val == Animal{}) { 280 | t.Error("Expecting an object.") 281 | } 282 | counter++ 283 | } 284 | 285 | if counter != 100 { 286 | t.Error("We should have counted 100 elements.") 287 | } 288 | } 289 | 290 | func TestBufferedIterator(t *testing.T) { 291 | m := New[string, Animal]() 292 | 293 | // Insert 100 elements. 294 | for i := 0; i < 100; i++ { 295 | m.Set(strconv.Itoa(i), Animal{strconv.Itoa(i)}) 296 | } 297 | 298 | counter := 0 299 | // Iterate over elements. 300 | for item := range m.IterBuffered() { 301 | val := item.Val 302 | 303 | if (val == Animal{}) { 304 | t.Error("Expecting an object.") 305 | } 306 | counter++ 307 | } 308 | 309 | if counter != 100 { 310 | t.Error("We should have counted 100 elements.") 311 | } 312 | } 313 | 314 | func TestClear(t *testing.T) { 315 | m := New[string, Animal]() 316 | 317 | // Insert 100 elements. 318 | for i := 0; i < 100; i++ { 319 | m.Set(strconv.Itoa(i), Animal{strconv.Itoa(i)}) 320 | } 321 | 322 | m.Clear() 323 | 324 | if m.Count() != 0 { 325 | t.Error("We should have 0 elements.") 326 | } 327 | } 328 | 329 | func TestIterCb(t *testing.T) { 330 | m := New[string, Animal]() 331 | 332 | // Insert 100 elements. 333 | for i := 0; i < 100; i++ { 334 | m.Set(strconv.Itoa(i), Animal{strconv.Itoa(i)}) 335 | } 336 | 337 | counter := 0 338 | // Iterate over elements. 339 | m.IterCb(func(key string, v Animal) { 340 | counter++ 341 | }) 342 | if counter != 100 { 343 | t.Error("We should have counted 100 elements.") 344 | } 345 | } 346 | 347 | func TestItems(t *testing.T) { 348 | m := New[string, Animal]() 349 | 350 | // Insert 100 elements. 351 | for i := 0; i < 100; i++ { 352 | m.Set(strconv.Itoa(i), Animal{strconv.Itoa(i)}) 353 | } 354 | 355 | items := m.Items() 356 | 357 | if len(items) != 100 { 358 | t.Error("We should have counted 100 elements.") 359 | } 360 | } 361 | 362 | func TestConcurrent(t *testing.T) { 363 | m := New[string, int]() 364 | ch := make(chan int) 365 | const iterations = 1000 366 | var a [iterations]int 367 | 368 | // Using go routines insert 1000 ints into our map. 369 | go func() { 370 | for i := 0; i < iterations/2; i++ { 371 | // Add item to map. 372 | m.Set(strconv.Itoa(i), i) 373 | 374 | // Retrieve item from map. 375 | val, _ := m.Get(strconv.Itoa(i)) 376 | 377 | if val != i { 378 | fmt.Println("@@@@:", i, val) 379 | } 380 | 381 | // Write to channel inserted value. 382 | ch <- val 383 | } // Call go routine with current index. 384 | }() 385 | 386 | go func() { 387 | for i := iterations / 2; i < iterations; i++ { 388 | // Add item to map. 389 | m.Set(strconv.Itoa(i), i) 390 | 391 | // Retrieve item from map. 392 | val, _ := m.Get(strconv.Itoa(i)) 393 | 394 | // Write to channel inserted value. 395 | ch <- val 396 | } // Call go routine with current index. 397 | }() 398 | 399 | // Wait for all go routines to finish. 400 | counter := 0 401 | for elem := range ch { 402 | a[counter] = elem 403 | counter++ 404 | if counter == iterations { 405 | break 406 | } 407 | } 408 | 409 | // Sorts array, will make is simpler to verify all inserted values we're returned. 410 | sort.Ints(a[0:iterations]) 411 | 412 | // Make sure map contains 1000 elements. 413 | if m.Count() != iterations { 414 | t.Error("Expecting 1000 elements.") 415 | } 416 | 417 | // Make sure all inserted values we're fetched from map. 418 | for i := 0; i < iterations; i++ { 419 | if i != a[i] { 420 | t.Error("missing value", i) 421 | } 422 | } 423 | } 424 | 425 | func TestJsonMarshal(t *testing.T) { 426 | ShardCount = 2 427 | defer func() { 428 | ShardCount = 32 429 | }() 430 | expected := "{\"a\":1,\"b\":2}" 431 | m := New[string, int]() 432 | m.Set("a", 1) 433 | m.Set("b", 2) 434 | j, err := json.Marshal(m) 435 | if err != nil { 436 | t.Error(err) 437 | } 438 | 439 | if string(j) != expected { 440 | t.Error("json", string(j), "differ from expected", expected) 441 | return 442 | } 443 | } 444 | 445 | func TestKeys(t *testing.T) { 446 | m := New[string, Animal]() 447 | 448 | // Insert 100 elements. 449 | for i := 0; i < 100; i++ { 450 | m.Set(strconv.Itoa(i), Animal{strconv.Itoa(i)}) 451 | } 452 | 453 | keys := m.Keys() 454 | if len(keys) != 100 { 455 | t.Error("We should have counted 100 elements.") 456 | } 457 | } 458 | 459 | func TestMInsert(t *testing.T) { 460 | animals := map[string]Animal{ 461 | "elephant": Animal{"elephant"}, 462 | "monkey": Animal{"monkey"}, 463 | } 464 | m := New[string, Animal]() 465 | m.MSet(animals) 466 | 467 | if m.Count() != 2 { 468 | t.Error("map should contain exactly two elements.") 469 | } 470 | } 471 | 472 | func TestUpsert(t *testing.T) { 473 | dolphin := Animal{"dolphin"} 474 | whale := Animal{"whale"} 475 | tiger := Animal{"tiger"} 476 | lion := Animal{"lion"} 477 | 478 | cb := func(exists bool, valueInMap Animal, newValue Animal) Animal { 479 | if !exists { 480 | return newValue 481 | } 482 | valueInMap.name += newValue.name 483 | return valueInMap 484 | } 485 | 486 | m := New[string, Animal]() 487 | m.Set("marine", dolphin) 488 | m.Upsert("marine", whale, cb) 489 | m.Upsert("predator", tiger, cb) 490 | m.Upsert("predator", lion, cb) 491 | 492 | if m.Count() != 2 { 493 | t.Error("map should contain exactly two elements.") 494 | } 495 | 496 | marineAnimals, ok := m.Get("marine") 497 | if marineAnimals.name != "dolphinwhale" || !ok { 498 | t.Error("Set, then Upsert failed") 499 | } 500 | 501 | predators, ok := m.Get("predator") 502 | if !ok || predators.name != "tigerlion" { 503 | t.Error("Upsert, then Upsert failed") 504 | } 505 | } 506 | 507 | func TestKeysWhenRemoving(t *testing.T) { 508 | m := New[string, Animal]() 509 | 510 | // Insert 100 elements. 511 | Total := 100 512 | for i := 0; i < Total; i++ { 513 | m.Set(strconv.Itoa(i), Animal{strconv.Itoa(i)}) 514 | } 515 | 516 | // Remove 10 elements concurrently. 517 | Num := 10 518 | for i := 0; i < Num; i++ { 519 | go func(c *SafeMap[string, Animal], n int) { 520 | c.Remove(strconv.Itoa(n)) 521 | }(m, i) 522 | } 523 | keys := m.Keys() 524 | for _, k := range keys { 525 | if k == "" { 526 | t.Error("Empty keys returned") 527 | } 528 | } 529 | } 530 | 531 | func TestUnDrainedIter(t *testing.T) { 532 | m := New[string, Animal]() 533 | // Insert 100 elements. 534 | Total := 100 535 | for i := 0; i < Total; i++ { 536 | m.Set(strconv.Itoa(i), Animal{strconv.Itoa(i)}) 537 | } 538 | counter := 0 539 | // Iterate over elements. 540 | ch := m.Iter() 541 | for item := range ch { 542 | val := item.Val 543 | 544 | if (val == Animal{}) { 545 | t.Error("Expecting an object.") 546 | } 547 | counter++ 548 | if counter == 42 { 549 | break 550 | } 551 | } 552 | for i := Total; i < 2*Total; i++ { 553 | m.Set(strconv.Itoa(i), Animal{strconv.Itoa(i)}) 554 | } 555 | for item := range ch { 556 | val := item.Val 557 | 558 | if (val == Animal{}) { 559 | t.Error("Expecting an object.") 560 | } 561 | counter++ 562 | } 563 | 564 | if counter != 100 { 565 | t.Error("We should have been right where we stopped") 566 | } 567 | 568 | counter = 0 569 | for item := range m.IterBuffered() { 570 | val := item.Val 571 | 572 | if (val == Animal{}) { 573 | t.Error("Expecting an object.") 574 | } 575 | counter++ 576 | } 577 | 578 | if counter != 200 { 579 | t.Error("We should have counted 200 elements.") 580 | } 581 | } 582 | 583 | func TestUnDrainedIterBuffered(t *testing.T) { 584 | m := New[string, Animal]() 585 | // Insert 100 elements. 586 | Total := 100 587 | for i := 0; i < Total; i++ { 588 | m.Set(strconv.Itoa(i), Animal{strconv.Itoa(i)}) 589 | } 590 | counter := 0 591 | // Iterate over elements. 592 | ch := m.IterBuffered() 593 | for item := range ch { 594 | val := item.Val 595 | 596 | if (val == Animal{}) { 597 | t.Error("Expecting an object.") 598 | } 599 | counter++ 600 | if counter == 42 { 601 | break 602 | } 603 | } 604 | for i := Total; i < 2*Total; i++ { 605 | m.Set(strconv.Itoa(i), Animal{strconv.Itoa(i)}) 606 | } 607 | for item := range ch { 608 | val := item.Val 609 | 610 | if (val == Animal{}) { 611 | t.Error("Expecting an object.") 612 | } 613 | counter++ 614 | } 615 | 616 | if counter != 100 { 617 | t.Error("We should have been right where we stopped") 618 | } 619 | 620 | counter = 0 621 | for item := range m.IterBuffered() { 622 | val := item.Val 623 | 624 | if (val == Animal{}) { 625 | t.Error("Expecting an object.") 626 | } 627 | counter++ 628 | } 629 | 630 | if counter != 200 { 631 | t.Error("We should have counted 200 elements.") 632 | } 633 | } 634 | --------------------------------------------------------------------------------