Stdlib Generic Packages — Professional Level¶
Table of Contents¶
- The migration story
- Release-by-release additions
- Code review patterns to enforce stdlib usage
- Team-level adoption playbook
- Summary
The migration story¶
Before Go 1.21 every team had its own util or pkg/internal directory full of helpers like ContainsString, MapKeys, MaxInt, SortByName. The migration to stdlib generics replaces those helpers, but the shape of the migration matters.
From sort.Slice to slices.SortFunc¶
The pre-1.21 idiom for sorting a struct slice:
Two annoyances: 1. The closure indexes back into people, which is verbose. 2. The comparator returns bool, not the standard tri-state int.
The post-1.21 idiom:
The closure receives elements directly, the comparator is the universal cmp.Compare form, and the function name says "sort a slice", not "sort by index".
Both styles still compile. sort.Slice will not be removed — it pre-dates generics and is part of the Go 1 compatibility promise. But new code should use slices.SortFunc.
From manual loops to slices.Contains¶
// Before
found := false
for _, v := range s {
if v == target { found = true; break }
}
// After
found := slices.Contains(s, target)
One line, no off-by-one risk.
From for k := range m { keys = append(keys, k) } to maps.Keys¶
// Before (Go 1.20)
keys := make([]string, 0, len(m))
for k := range m { keys = append(keys, k) }
// Go 1.21–1.22
keys := maps.Keys(m) // returned []K
// Go 1.23+
keys := slices.Collect(maps.Keys(m)) // maps.Keys returns iter.Seq[K]
From if a != "" { return a }; if b != "" { return b } to cmp.Or¶
// Before
name := func() string {
if userInput != "" { return userInput }
if configValue != "" { return configValue }
return "default"
}()
// After (1.22+)
name := cmp.Or(userInput, configValue, "default")
This idiom alone saves dozens of lines across a typical codebase.
Release-by-release additions¶
The stdlib generic surface grew incrementally. Knowing what is in which release is essential for matching go.mod directives to the API you intend to use.
Go 1.21 (August 2023)¶
The big bang. Notable additions:
slicespackage promoted fromgolang.org/x/exp/slicesmapspackage promoted fromgolang.org/x/exp/mapscmppackage, includingcmp.Ordered,cmp.Compare,cmp.Lessmin,max,clearas built-in functionsslices.Min,slices.Max,slices.MinFunc,slices.MaxFuncslices.BinarySearch,slices.BinarySearchFunc
Release notes: https://go.dev/doc/go1.21#minor_library_changes
Go 1.22 (February 2024)¶
slices.Concat— concatenate any number of slices in one allocationcmp.Or— return first non-zero argumentslices.Repeat— repeat a slice N times
Release notes: https://go.dev/doc/go1.22
Go 1.23 (August 2024)¶
iterpackage —iter.Seq[T]anditer.Seq2[K, V]slices.All,slices.Values,slices.Backward— return iterators over a sliceslices.Sorted,slices.SortedFunc,slices.SortedStableFunc— collect from iterator into sorted sliceslices.Collect—iter.Seq[T]to[]Tslices.AppendSeq— append iterator output to existing slicemaps.Keys,maps.Valuesswitched to returniter.Seq[K]/iter.Seq[V]maps.All,maps.Collect— adapters between iterators and maps- Range-over-func became a stable language feature
Release notes: https://go.dev/doc/go1.23
Go 1.24 (February 2025)¶
- Generic type aliases (
type Vec[T any] = []T) — fully supported, enabling cleaner type wrappers across the stdlib weak.Pointer[T]— generic weak pointer inweakpackage- Fine-tuned performance improvements in
slices.Sort
Release notes: https://go.dev/doc/go1.24
Compatibility matrix¶
| Need | Minimum Go version |
|---|---|
slices.Sort, cmp.Ordered | 1.21 |
cmp.Or, slices.Concat | 1.22 |
iter.Seq, slices.Collect, iterator-returning maps.Keys | 1.23 |
| Generic type aliases | 1.24 |
Pin go 1.23 in go.mod if you want the iterator API; pin 1.22 if cmp.Or is enough.
Code review patterns to enforce stdlib usage¶
A team that has migrated to generic stdlib packages benefits from review rules. Recommended rules:
Rule 1 — No hand-rolled Contains / Index / Sort helpers¶
If a reviewer sees:
…they ask: "Why not slices.Contains?" Unless there is a reason (older Go version, custom equality), the helper is replaced.
Rule 2 — slices.SortFunc(s, cmp.Compare) over sort.Slice¶
For new code, prefer the typed comparator. Only keep sort.Slice if the file is supporting Go < 1.21.
Rule 3 — cmp.Or for first-non-zero chains¶
The pattern:
…is flagged in review and rewritten as cmp.Or(a, b, c).
Rule 4 — maps.Clone over manual map copy loops¶
The stdlib version is faster and clearer.
Rule 5 — Watch for unused returns¶
Every reviewer should know that slices.Insert, slices.Delete, and slices.Compact must be reassigned. A bare call without assignment is almost always a bug.
Rule 6 — Pre-size collections¶
If you know the final size, pre-size:
Some teams add a linter rule for this.
Rule 7 — Don't reinvent cmp.Ordered¶
If a team-internal Ordered constraint exists, replace it with cmp.Ordered. Multiple constraints with the same meaning create confusion.
Tooling¶
goplsflags some of these patterns automatically.golangci-linthas rules for unused return values.- A team-internal
forbidigorule can bansort.Slicein new files. staticcheck(SA1029family) catches some misuse but not this pattern set.
Team-level adoption playbook¶
A working playbook used by several teams that migrated successfully:
Phase 1 — Baseline¶
- Bump
go.modtogo 1.21(or whichever minimum your dependents allow). - Update CI to use the same toolchain.
- Run
go vetandstaticcheckon the existing tree to surface pre-existing issues that the new linter rules might double-report.
Phase 2 — Discover and replace¶
- Grep for hand-written
Contains,MapKeys,MaxInt, etc. - Replace with stdlib calls.
- Add migration commits as small PRs grouped by module.
Phase 3 — Lock the new style¶
- Add forbidigo or staticcheck rules to ban
sort.Slicein new files. - Update CONTRIBUTING.md with the patterns above.
- Run a one-time
goplsquick-fix sweep.
Phase 4 — Iterator APIs¶
- When you bump to
go 1.23, decide if you wantmaps.Keysto returniter.Seq[K]. Most code paths can useslices.Collect(maps.Keys(m))to keep the slice idiom. - New code can use
for k := range maps.Keys(m)directly for cleaner loops.
Common migration mistakes¶
- Removing
sort.Slicefrom packages used by older consumers. Backward compatibility breaks. - Replacing
interface{}helpers without checking call sites. Consumers may depend on the looser API. - Bumping
go.modpast what users have installed. Coordinate with downstream. - Mixing
maps.Keys(1.21 slice form) andslices.Collect(maps.Keys(...))(1.23 form) in one PR. Pick one and stick.
Case studies¶
Case 1 — A logging library¶
A team maintained a logging library with custom helpers containsString, dedupStrings, and sortByLevel. Migration:
- Replaced
containsStringwithslices.Contains— 12 call sites, one PR. - Replaced
dedupStringswithslices.Sort+slices.Compact— semantics matched. sortByLevelbecameslices.SortFuncwithcmp.Compare(a.Level, b.Level).
Result: removed 80 lines of utility code, no behaviour changes, faster benchmarks (the stdlib pdqsort beat the library's heapsort).
Case 2 — A data pipeline¶
A pipeline processed batches of records, deduplicating by key and sorting by timestamp. Original code used sort.Slice and a manual map[string]struct{} dedup loop.
Migration replaced both with:
slices.SortStableFunc(batch, func(a, b Record) int {
return cmp.Or(
cmp.Compare(a.Key, b.Key),
cmp.Compare(a.Time.Unix(), b.Time.Unix()),
)
})
batch = slices.CompactFunc(batch, func(a, b Record) bool {
return a.Key == b.Key
})
Six lines replaced 30. Benchmarks improved by 12% on average batches.
Case 3 — A configuration loader¶
The loader resolved config values from environment, file, and defaults. The original code:
…replaced with cmp.Or(os.Getenv("FOO"), file["FOO"], "default"). Across the codebase: 60 lines saved, every chain became a one-liner.
The catch: cmp.Or evaluates all arguments. The team kept the explicit chain in places where lookup was expensive (loadFromNetwork()).
Versioning your go.mod¶
Lock the minimum Go version that supports the stdlib API you use:
// go.mod
module example.com/myapp
go 1.21 // for slices, maps, cmp
// or
go 1.22 // for cmp.Or, slices.Concat
// or
go 1.23 // for iter.Seq, slices.Collect, iterator-returning maps.Keys
// or
go 1.24 // for generic type aliases, weak.Pointer
Choose the minimum that works for the API you actually use. Bumping for "the latest" introduces compatibility friction with users on older toolchains.
For libraries with broad use, follow the previous-stable convention: support N-1 and N-2 stable Go releases. As of 2026, that means supporting 1.23 and 1.24 if 1.25 is current.
Summary¶
The migration to slices, maps, and cmp is incremental, not "big bang". Teams that adopt them well:
- Track release notes — know which API landed in which release.
- Lock minimum Go version in
go.modand CI. - Replace hand-written helpers with stdlib calls in small PRs.
- Enforce review rules —
slices.SortFunc(s, cmp.Compare)oversort.Slice,cmp.Orover chained ifs. - Coordinate with downstream consumers before bumping the toolchain.
- Treat the iterator switch (1.23) as a separate, deliberate migration.
The stdlib generic packages are the foundation of modern Go style. A team that has internalized them writes shorter, clearer, faster code — and onboards new engineers more quickly because the idioms are universal.
Move on to specification.md for accurate signatures and pkg-doc excerpts of the most important functions.