Go slices and maps
Skill ctoth/golang-skills-plugin/plugins/golang/skills/go-slices-and-maps
Guides Go slice and map data operations and the slices/maps stdlib packages — a slice is a view (pointer, len, cap) over a backing array, so subslices alias and append conditionally mutates a caller's shared array (the #1 slice bug); use a 3-index slice or slices.Clone/Clip to break aliasing and bound memory; a nil map panics on write but reads fine; map iteration order is randomized by spec; prefer slices.Contains/Sort/Equal and maps.Clone/Keys over hand-rolled loops. Auto-invokes when writing or editing slice operations, append, slicing expressions, make([]T...), maps, map iteration, or on "why did this slice change", "why is map order different", or "why did writing to this map panic". The data layer under every Go program.From its SKILL.md
npx -y skills add ctoth/golang-skills-plugin --skill go-slices-and-mapsAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
15.7 KB, ~4.0k tokens by cl100k_base, as published. Nobody here has run it
Go Slices and Maps
"A slice is a descriptor of an array segment. It consists of a pointer to the array, the length of the segment, and its capacity (the maximum length of the segment)." — Go Slices: usage and internals
"Slicing does not copy the slice's data. It creates a new slice value that points to the original array." — Go Slices: usage and internals
A slice is not a container; it is a view. Three words — pointer, length, capacity — over a backing array that the slice does not own and may share with other slices. Almost every slice surprise ("why did my original change?", "why is this 2 GB still in memory?") follows from that one fact. Maps have their own small set of sharp edges: a nil map panics on write, a missing key reads as the zero value, and iteration order is deliberately randomized. This skill owns those data operations and the generic slices/maps/builtin helpers that tame them.
1. The Headline Fact: A Slice Is a View, append Conditionally Mutates
A slice header is (ptr, len, cap). Slicing shares the backing array — "modifying the elements of a re-slice modifies the elements of the original slice" (Slices intro). append then has two behaviors decided at runtime: "If it has sufficient capacity, the destination is resliced to accommodate the new elements. If it does not, a new underlying array will be allocated" (builtin.append). When capacity suffices, append writes into the shared backing array — clobbering whatever a caller still holds there. That conditional is the single most common slice bug.
// WRONG — append into a subslice silently mutates the original
orig := []int{1, 2, 3, 4}
sub := orig[:2] // len 2, cap 4 — shares orig's backing array
sub = append(sub, 99) // cap suffices: writes orig[2]; orig is now [1 2 99 4]
// RIGHT — cap the capacity (3-index) so append must reallocate, or Clone first
sub := orig[:2:2] // s[low:high:max]; cap now 2, append reallocates
sub2 := slices.Clone(orig[:2]) // independent backing array
This is verified behavior: a passing test in this skill's research showed append(orig[:2], 99) leaving orig == [1 2 99 4], while orig[:2:2] and slices.Clone left orig == [1 2 3 4]. Always store append's result (s = append(s, ...)): "since the slice header is always updated by a call to append, you need to save the returned slice after the call" (Mechanics of append).
2. The Rules and Their Sources
| Rule | The discipline | Source |
|---|---|---|
| Slice is a view | (ptr, len, cap); slicing shares the array, it does not copy | "Slicing does not copy the slice's data ... points to the original array" (Slices intro) |
| append may alias or realloc | Cap suffices → mutates shared array; else reallocates | "If it has sufficient capacity, the destination is resliced ... If it does not, a new underlying array will be allocated" (builtin) |
| Always store append's result | The header (len/cap/ptr) changes; the old value is stale | "you need to save the returned slice after the call" (Mechanics of append) |
| Break aliasing deliberately | 3-index s[lo:hi:max], slices.Clip, or slices.Clone/copy | "Clip removes unused capacity"; "Clone returns a copy ... shallow clone" (slices) |
| Subslices retain the whole array | A small subslice keeps the entire backing array alive | "The full array will be kept in memory until it is no longer referenced" (Slices intro) |
| Preallocate when len is known | make([]T, 0, n) to avoid repeated regrowth | make([]int, 0, 10) "allocates an underlying array of size 10" (builtin) |
Prefer nil over []T{} | var s []T is nil but fully usable | "the nil slice is the preferred style" (CodeReviewComments) |
| nil map panics on write | Must make before assigning; reads return the zero value | "A nil map is equivalent to an empty map except that no elements may be added" (Spec — Map types) |
| Map order is randomized | Never depend on range order; sort keys for determinism | "The iteration order is not specified and is not guaranteed to be the same from one call to the next" (maps) |
| Prefer the stdlib | slices.Contains/Sort/Equal, maps.Clone/Keys over loops | slices, maps |
3. Aliasing and Memory Retention: Clip, Clone, copy
Two distinct problems flow from the shared backing array.
Mutation aliasing (§1): a subslice and its parent write through the same memory. Break it with a 3-index slice to cap capacity so the next append must reallocate, or copy the data outright. slices.Clip does the cap fix as a named operation: "Clip removes unused capacity from the slice, returning s[:len(s):len(s)]" (slices.Clip). slices.Clone makes an independent copy — but note it is shallow: "The elements are copied using assignment, so this is a shallow clone" (slices.Clone). A []*T or [][]byte Clone shares the pointed-to data.
Memory retention (the leak): because re-slicing never copies, "The full array will be kept in memory until it is no longer referenced. Occasionally this can cause the program to hold all the data in memory when only a small piece of it is needed" (Slices intro). Returning bigBuffer[:3] keeps the entire bigBuffer alive. slices.Clone (or copy into a right-sized slice) lets the big array be collected.
// WRONG — the returned 3-byte slice pins a 10 MB backing array forever
func firstThree(data []byte) []byte { return data[:3] }
// RIGHT — copy the bytes you need; the big array can be collected
func firstThree(data []byte) []byte { return slices.Clone(data[:3]) }
4. Preallocation and nil-vs-empty Slices
When the final length is known, preallocate: make([]T, 0, n) reserves capacity so append doesn't repeatedly grow and copy the backing array. make([]int, 0, 10) "allocates an underlying array of size 10 and returns a slice of length 0 and capacity 10" (builtin.make). (Watch the second arg: make([]T, n) gives length n of zeroes — appending then adds past them.)
A nil slice is not a problem to fix — it is the idiomatic empty slice. var s []T is nil, yet len(s), range s, and append(s, ...) all work. Prefer it: "The former declares a nil slice value, while the latter is non-nil but zero-length. They are functionally equivalent ... but the nil slice is the preferred style" (CodeReviewComments). And do not build APIs that distinguish the two: "Do not create APIs that force their clients to make distinctions between nil and the empty slice" (Google Style — Decisions).
The one place the distinction is real is JSON: a nil slice marshals to null, while []T{} marshals to []. "a nil slice encodes to null, while []string{} encodes to the JSON array []" (CodeReviewComments). When a client needs [], use []T{} deliberately — that contract is owned by go-json.
5. The slices Package — Stop Hand-Rolling Loops
Since Go 1.21 the slices package ships the generic operations people used to re-implement (and get subtly wrong). Reach for these before writing a loop:
slices.Contains(s, v) // func Contains[S ~[]E, E comparable](s S, v E) bool
slices.Index(s, v) // first index of v, or -1
slices.Sort(xs) // ascending, cmp.Ordered elements
slices.SortFunc(xs, cmp) // custom order; cmp returns <0 / 0 / >0
slices.Equal(a, b) // same length and all elements ==; nil == empty
slices.Max(xs); slices.Min(xs) // panic if xs is empty
slices.BinarySearch(xs, t) // requires xs sorted ascending
Three mutating helpers have semantics worth remembering:
slices.Delete(s, i, j)removess[i:j]and "Delete zeroes the elementss[len(s)-(j-i):len(s)]" (slices.Delete) — it returns a shorter slice over the same array; use the returned value.slices.Insert(s, i, v...)shifts elements up and returns the grown slice.slices.Compact(s)"replaces consecutive runs of equal elements with a single copy" (slices.Compact) — likeuniq, so sort first for a global dedupe.
These are generic functions; the type-parameter mechanics ([S ~[]E, E comparable], why ~[]E admits named slice types) are owned by go-generics §6. For non-comparable elements (a struct with a slice field), == won't compile — use slices.ContainsFunc/slices.EqualFunc.
6. Maps: nil Writes Panic, Missing Keys, comma-ok, Randomized Order
The map zero value is nil, and it is read-only: "A nil map is equivalent to an empty map except that no elements may be added" (Spec — Map types). Writing to it is a runtime panic — verified: a recover-guarded test confirmed m["x"] = 1 on a var m map[string]int panics, while make(map[string]int) works. make the map (or use a composite literal) before any write.
Reading a missing key is not an error — it returns the value type's zero value. Distinguish "absent" from "present-but-zero" with the comma-ok form:
var m map[string]int // nil
_ = m["x"] // OK: reads 0 (reading a nil map is fine)
m["x"] = 1 // PANIC: assignment to entry in nil map
m = make(map[string]int)
m["x"] = 1 // OK now
n := m["missing"] // n == 0, no error
n, ok := m["missing"] // ok == false distinguishes absent from a stored 0
Iteration order is randomized by design. "The iteration order is not specified and is not guaranteed to be the same from one call to the next" (maps.Keys) — the same is stated for range in the spec. Code that depends on range order is broken; a test here saw 8 distinct "first keys" over 200 range loops of one map. For deterministic output, collect the keys and sort them:
keys := make([]string, 0, len(m))
for k := range m {
keys = append(keys, k)
}
sort.Strings(keys) // or slices.Sorted(maps.Keys(m)) on Go 1.23+
for _, k := range keys {
fmt.Println(k, m[k])
}
delete(m, k) during a range is safe — the spec permits deleting the current or not-yet-reached entries. Concurrent map access is a different matter: a concurrent read+write is a fatal runtime error, not a data race you can ignore — routed to go-race-and-memory-model and go-sync-primitives (sync.RWMutex or sync.Map).
7. The maps Package and clear
The maps package (Go 1.21; iterators added 1.23) covers the common map operations:
maps.Clone(m) // shallow copy: "the new keys and values are set using ordinary assignment"
maps.Copy(dst, src) // merge src into dst, overwriting on key collision
maps.Equal(a, b) // same key/value pairs, values compared with ==
maps.DeleteFunc(m, fn) // delete entries where fn(k, v) is true
for k := range maps.Keys(m) { ... } // iter.Seq[K], Go 1.23+
maps.Clone is shallow, exactly like slices.Clone: a map[string][]int clone shares the value slices. The builtin clear (Go 1.21) empties either: "For maps, clear deletes all entries, resulting in an empty map. For slices, clear sets all elements up to the length of the slice to the zero value" (builtin.clear). clear(m) is the idiom to reuse a map allocation; s = s[:0] reuses a slice's backing array (keeping capacity) when you want to refill it. Version gating (slices/maps/clear at 1.21, iterators at 1.23) is owned by go-version-feature-map.
8. Who Suffers When This Is Done Badly
The cost of a slice or map mistake lands on someone other than the author, often far away:
- The caller who passed a slice to your function, kept using it, and watched its elements change under them — because your
appendhad spare capacity and wrote through the shared backing array (§1). A singleslices.Cloneat the boundary would have prevented it. - The on-call engineer chasing OOM in a service that parses 2 KB out of every 50 MB upload and returns a subslice — pinning every upload's full buffer in the heap (§3).
- The next reader of a test that passes locally and fails in CI because it asserted on map range order (§6) — randomized output that looked stable on one machine.
- The user who hit a panic in production the first time a code path reached an un-
maked map (§6) — a nil-map write that no read ever exercised.
These are not edge cases; they are the default behavior of a view type and a panic-on-write map. Knowing the data model is what separates code that happens to work from code that is correct.
9. Routing to the Specific Skills
go-idiomatic-discipline— the policy root. Reinventingslices.Containsis axis 2 (over-building); a swallowed nil-vs-empty distinction is axis 1.go-generics—slices/mapsare generic packages; the[S ~[]E, E comparable]mechanics and "prefer the stdlib over hand-rolled type parameters" live there.go-race-and-memory-model— concurrent map read+write is a fatal error; the memory model behind it.go-sync-primitives— protecting a shared map withRWMutexor reaching forsync.Map.go-json— the nil-slice →nullvs[]T{}→[]marshalling contract.go-strings-bytes-runes—[]byteis a slice; the same aliasing and append rules apply to byte slices andstring↔[]byte.go-version-feature-map—slices/maps/clear(1.21),maps.Keys/Valuesiterators (1.23) gate on thegodirective.
10. Reference Files
High-frequency slice/map anti-patterns in LLM-generated Go, each with wrong/right code and citations:
${CLAUDE_SKILL_DIR}/references/common-mistakes.md
Source provenance for every claim in this skill:
${CLAUDE_SKILL_DIR}/references/sources.yaml
What ships with it: 2 files
18.8 KB alongside SKILL.md
references/
- common-mistakes.md11.0 KB
- sources.yaml7.7 KB