Stdlib Generic Packages — Senior Level¶
Table of Contents¶
- Algorithmic guarantees
- In-place vs copying APIs
- Aliasing pitfalls
- When stdlib is good enough
- When to roll your own
- Summary
Algorithmic guarantees¶
The Go standard library documents complexity, but a senior engineer must know what algorithm is actually used. The slices and maps packages give specific guarantees.
slices.Sort — pdqsort¶
slices.Sort and slices.SortFunc use pdqsort (pattern-defeating quicksort), a 2016 algorithm by Orson Peters. Its properties:
- Average:
O(n log n) - Worst case:
O(n log n)(unlike plain quicksort) - Memory:
O(log n)stack from recursion, no heap - Adaptive:
O(n)on already-sorted data - Unstable
The pdqsort implementation in slices was contributed by the Go team and is faster than the old sort.Sort for most workloads — typically 30-50% on numeric slices, sometimes 2-3× on partially sorted data.
slices.SortStableFunc — symmerge / mergesort hybrid¶
The stable variant uses a stable in-place merge sort, with O(n log n) time and O(log² n) extra stack. It is around 1.5-2× slower than the unstable version on random data.
slices.BinarySearch — guaranteed lower-bound¶
Returns the index of the first element >= target if not found exactly. This makes it a "lower bound" search, not just a "find anything". The Go team documents this explicitly so callers can rely on it.
slices.Contains — linear¶
O(n) always. There is no early termination beyond the obvious "stop on first match". For sorted data, prefer BinarySearch.
maps.Clone — O(n) with allocator hints¶
maps.Clone calls into the runtime via runtime.mapclone (an unexported helper). It pre-sizes the destination map to the source size and copies entries with the runtime's internal layout knowledge — typically 2-3× faster than a for k, v := range src { dst[k] = v } loop.
slices.Equal — short-circuit linear¶
Returns false on length mismatch immediately. Otherwise, O(n) comparisons.
cmp.Compare — branchless on numeric types¶
For most numeric types, the compiler emits a branchless sequence (subtraction + sign extraction). For floats, NaN handling forces a branch. For strings, it delegates to runtime.cmpstring.
In-place vs copying APIs¶
A senior engineer cares about which functions mutate and which return new slices.
In-place (mutates the input)¶
| Function | Effect |
|---|---|
slices.Sort | sorts in place |
slices.SortFunc | sorts in place |
slices.Reverse | reverses in place |
maps.Copy(dst, src) | mutates dst |
maps.DeleteFunc(m, ...) | mutates m |
Returns a new slice¶
| Function | Returns |
|---|---|
slices.Clone | new slice |
slices.Concat | new slice (1.22+) |
slices.Sorted | new sorted slice (1.23+) |
maps.Clone | new map |
Mutates and returns¶
The trickiest category — these mutate the input and return a (possibly different) slice header:
| Function | Behaviour |
|---|---|
slices.Insert(s, i, v...) | may reallocate; you must reassign |
slices.Delete(s, i, j) | shifts elements left, zeroes tail; reassign |
slices.Compact(s) | shifts elements left; reassign |
slices.Replace | inserts and deletes; reassign |
Never write slices.Insert(s, 0, x) and discard the return value. The original s may now have stale length.
Aliasing pitfalls¶
slices.Compact, slices.Delete, and slices.Insert may alias the input. Knowing when is critical.
slices.Compact returns a prefix¶
s := []int{1, 1, 2, 3, 3}
out := slices.Compact(s)
// out is s[:3] = [1 2 3], aliasing the same backing array
// s[3] and s[4] are zero — Compact zeroes the tail for GC
If you keep both s and out, mutating out[i] mutates s[i]. If you keep s after Compact, the trailing zeros are surprising.
slices.Delete zeroes the tail¶
s := []int{1, 2, 3, 4, 5}
s = slices.Delete(s, 1, 3) // remove indices 1 and 2
// s is now [1, 4, 5]; the underlying array's positions 3, 4 are zeroed
The zeroing matters for slices of pointers — without it, the GC could not reclaim the removed entries. The cost is O(j - i) extra writes.
slices.Insert may reallocate¶
If cap(s) < len(s) + len(v), Insert allocates a new backing array. Otherwise it shifts in place. Two cases means you cannot rely on either pointer identity or absence of allocation.
slices.Clone is a shallow copy¶
maps.Clone and slices.Clone are shallow. If T is a pointer or contains pointers, the clone shares them with the original.
type Box struct { inner *int }
orig := []Box{{inner: new(int)}}
cp := slices.Clone(orig)
*cp[0].inner = 99
// orig[0].inner now points to 99 too — shared *int
For deep copies, write your own with the same pattern but a per-element clone callback.
maps.Copy doesn't allocate¶
maps.Copy(dst, src) writes each src entry into dst. It does not check whether dst is large enough — Go maps grow automatically. But the cost of growing while copying can dominate; pre-size with make(map[K]V, len(src)) first.
When stdlib is good enough¶
The slices, maps, and cmp packages cover the majority of slice and map operations a Go program needs. A senior engineer reaches for them when:
- The operation is named in the godoc — do not reinvent.
- The performance is not in the top 1% hot path — stdlib is well within 10% of any reasonable implementation.
- Type safety matters more than micro-optimization.
- Testability is helped by using a well-known function name. Reviewers immediately understand
slices.Contains(s, v). - Cross-team or cross-project consistency — every team uses the same primitives.
Cases the stdlib already handles well¶
- Sorting any
[]Twithcmp.Orderedkeys - Multi-key sorting via
cmp.Orchains - Set-membership with
slices.Contains - Lower-bound search with
BinarySearch - Map cloning, key iteration, deletion by predicate
- Sorted dedup with
Sort+Compact - "First non-zero" with
cmp.Or
When to roll your own¶
Stdlib is not always the right answer. A senior engineer recognises the cases.
1. Missing operations¶
The slices package does not include:
Reduce/FoldGroupByChunk/WindowZipWithUnique(it hasCompactonly after sorting)
If you need these, write them. Don't hack slices.X to fake them.
2. Specialized data structures¶
maps does not give you LRU, TTL, or concurrent maps. For those, third-party packages (hashicorp/golang-lru/v2, puzpuzpuz/xsync) or hand-rolled solutions are correct.
3. Performance hot paths¶
For numeric kernels processing tens of millions of elements per second, hand-tuned SIMD-friendly code may beat slices.Sort. Examples: sorting []float32 with a known small range can use radix sort. The stdlib intentionally chooses general-purpose algorithms.
4. Custom invariants¶
If your slice maintains an invariant (e.g., "sorted by Score with no duplicates"), wrapping the slice with a custom type and methods preserves the invariant better than calling raw slices.X.
type SortedScores []Score
func (s *SortedScores) Add(v Score) {
idx, _ := slices.BinarySearchFunc(*s, v, byScore)
*s = slices.Insert(*s, idx, v)
}
The wrapping type ensures every insertion goes through the right call.
5. Domain-specific equality¶
slices.Contains uses ==. If your equality is "same UUID even if other fields differ", use slices.ContainsFunc. Sometimes the predicate is so specific it deserves its own named function.
6. Generation semantics¶
The stdlib zeroes deleted slots. If your code already overwrites the tail, the extra writes are wasted. In that narrow case a hand-rolled Delete may be faster — but verify with benchmarks.
API design lessons from the stdlib¶
The Go team's choices in slices, maps, cmp carry lessons for any generic library:
Lesson 1 — Plain function for the common case, Func for flexibility¶
Every search/sort/compare in slices ships in two forms. The plain form requires a tighter constraint and gives clean call sites. The Func form takes a callback. Splitting these avoids forcing every user to write a comparator.
Lesson 2 — ~[]E and ~map[K]V are mandatory¶
Without the tilde, named slice types would not satisfy the constraint. type IDs []int; slices.Contains(ids, 5) fails. The Go team learned from early golang.org/x/exp/slices feedback and the stdlib went straight to ~[]E.
Lesson 3 — Panics for clear preconditions¶
slices.Min([]) panics. The team chose this over (T, error) because: - Min is one expression in 99% of call sites; an error return clutters every line - Empty input is a programmer bug, not a runtime condition - The panic message is documented and discoverable
Lesson 4 — Mutate-and-return for slice mutation¶
Insert, Delete, Compact all return S. Forces the caller to reassign — which is correct given that the slice header may change.
Lesson 5 — One small helper package per concern¶
cmp contains three functions and one constraint. Tiny. The team resisted putting Compare and Or into slices to keep concepts separate. A user importing cmp reads the godoc in 30 seconds.
These lessons are worth applying to internal generic helpers your team writes.
Summary¶
A senior engineer treats slices, maps, and cmp as the default toolset, with deliberate awareness of:
- Pdqsort (unstable) vs the stable variant
- In-place vs copying vs mutate-and-return semantics
- Aliasing of
Compact,Delete,Insert - Shallow-copy semantics of
Clone - Missing operations that justify rolling your own
- Performance ceilings where SIMD or domain-specific algorithms beat the general one
The standard library's stance is "good enough for almost everything, predictable and well-documented". A senior engineer reaches for it first, profiles when speed matters, and writes custom code only when the stdlib genuinely does not fit.
Move on to professional.md for the migration story across releases 1.21–1.24 and the team-level patterns that make these packages stick.