Table of contents
Part of nurago, a collection of independent Go packages for backend services.
import "github.com/tecnickcom/nurago/pkg/maputil"
Four functions over Go maps: Filter, Map, Reduce, Invert. None of them mutate the input.
The gap this fills
Go’s standard maps package covers copying and comparison (Clone, Copy, DeleteFunc, Equal, EqualFunc) and, since the iterator work landed, traversal (All, Keys, Values, Insert, Collect). It stops short of the transforms. There is no maps.Filter, no maps.Map, no maps.Reduce, and no inverse.
So every codebase grows its own. The loop is four lines and nobody reviews it closely:
out := make(map[string]int, len(in))
for k, v := range in {
if keep(k, v) {
out[k] = v
}
}
Written once, that is fine. Written forty times across a service, it accumulates the usual variations: one version forgets to pre-size, another mutates the input, a third silently drops entries on key collision. maputil exists to write it once.
Named map types survive
The signature constrains on ~map[K]V rather than on map[K]V:
func Filter[M ~map[K]V, K comparable, V any](m M, f func(K, V) bool) M
The practical difference shows up when the map has a name of its own:
type Registry map[string]Handler
var r Registry
filtered := maputil.Filter(r, isActive) // still a Registry, not a map[string]Handler
A helper declared as func Filter[K comparable, V any](m map[K]V, ...) map[K]V compiles against Registry but hands back the unnamed type, and any method set on Registry is lost. Map is the exception: it changes both key and value types, so it returns a plain map[J]U.
Two traps the doc comment is explicit about
Go randomises map iteration order on purpose, and both of these follow from it.
Reduce is only deterministic when the reducer is. Summing is fine. Subtracting is not, and neither is appending to a slice and expecting a stable order. Nothing in the type system catches this; the package documents it instead.
Map and Invert are last-write-wins. Invert a map that is not injective and you lose entries, unpredictably, because which survivor wins depends on the iteration order of that particular run. Invert on map[int]int{1: 10, 2: 10} returns a map of length one, and which key it holds may change between runs of the same binary.
Both behaviours are documented and tested here, rather than being rediscovered from a production incident.
Allocation behaviour
Filter pre-sizes its result to len(m), so it performs one allocation regardless of how many entries survive the predicate. Filtering a large map down to a few entries leaves the result holding capacity for the original size until it is copied.
Map allocates len(m) for a result that may be smaller, since colliding output keys collapse. Reduce allocates nothing beyond whatever the reducer does. Invert allocates len(m).
All four read the input under normal map semantics and never write to it, so they are safe to run on a map another goroutine is also reading, and unsafe to run on one another goroutine is writing. tsmap wraps these same four under a caller-supplied lock for that case.
Example
m := map[int]string{0: "Hello", 1: "World"}
filterFn := func(_ int, v string) bool { return v == "World" }
fmt.Println(maputil.Filter(m, filterFn))
// Output:
// map[1:World]
Full source is in example_maputil_test.go. More runnable examples are on pkg.go.dev.
Dependencies
This package reaches no external module: it uses only the Go standard library.
Links
- Reference documentation
- Source code
- Slice equivalents: sliceutil
- All nurago packages