# 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

- Skill: `ctoth/go-slices-and-maps` (Agent Skill, multi-file: 3 files)
- Install (CLI): `npx skillmds@latest add ctoth/go-slices-and-maps`
- Raw SKILL.md: https://api.skillmd.com/api/skills/ctoth/go-slices-and-maps/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: ctoth (https://skillmd.com/u/ctoth)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/ctoth/go-slices-and-maps

---


# 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](https://go.dev/blog/slices-intro)

> "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](https://go.dev/blog/slices-intro)

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](https://go.dev/blog/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](https://pkg.go.dev/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.

```go
// 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](https://go.dev/blog/slices)).

---

## 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](https://go.dev/blog/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](https://pkg.go.dev/builtin#append)) |
| 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](https://go.dev/blog/slices)) |
| 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](https://pkg.go.dev/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](https://go.dev/blog/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](https://pkg.go.dev/builtin#make)) |
| Prefer nil over `[]T{}` | `var s []T` is nil but fully usable | "the nil slice is the preferred style" ([CodeReviewComments](https://go.dev/wiki/CodeReviewComments#declaring-empty-slices)) |
| 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](https://go.dev/ref/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](https://pkg.go.dev/maps#Keys)) |
| Prefer the stdlib | `slices.Contains`/`Sort`/`Equal`, `maps.Clone`/`Keys` over loops | [slices](https://pkg.go.dev/slices), [maps](https://pkg.go.dev/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](https://pkg.go.dev/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](https://pkg.go.dev/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](https://go.dev/blog/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.

```go
// 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](https://pkg.go.dev/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](https://go.dev/wiki/CodeReviewComments#declaring-empty-slices)). 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](https://google.github.io/styleguide/go/decisions#nil-slices)).

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](https://go.dev/wiki/CodeReviewComments#declaring-empty-slices)). When a client needs `[]`, use `[]T{}` deliberately — that contract is owned by **[`go-json`](../go-json/SKILL.md)**.

---

## 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:

```go
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)`** removes `s[i:j]` and "Delete zeroes the elements `s[len(s)-(j-i):len(s)]`" ([slices.Delete](https://pkg.go.dev/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](https://pkg.go.dev/slices#Compact)) — like `uniq`, 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`](../go-generics/SKILL.md)** §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](https://go.dev/ref/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:

```go
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](https://pkg.go.dev/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:

```go
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`](../go-race-and-memory-model/SKILL.md)** and **[`go-sync-primitives`](../go-sync-primitives/SKILL.md)** (`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:

```go
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](https://pkg.go.dev/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`](../go-version-feature-map/SKILL.md)**.

---

## 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 `append` had spare capacity and wrote through the shared backing array (§1). A single `slices.Clone` at 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-`make`d 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`](../go-idiomatic-discipline/SKILL.md)** — the policy root. Reinventing `slices.Contains` is axis 2 (over-building); a swallowed nil-vs-empty distinction is axis 1.
- **[`go-generics`](../go-generics/SKILL.md)** — `slices`/`maps` are generic packages; the `[S ~[]E, E comparable]` mechanics and "prefer the stdlib over hand-rolled type parameters" live there.
- **[`go-race-and-memory-model`](../go-race-and-memory-model/SKILL.md)** — concurrent map read+write is a fatal error; the memory model behind it.
- **[`go-sync-primitives`](../go-sync-primitives/SKILL.md)** — protecting a shared map with `RWMutex` or reaching for `sync.Map`.
- **[`go-json`](../go-json/SKILL.md)** — the nil-slice → `null` vs `[]T{}` → `[]` marshalling contract.
- **[`go-strings-bytes-runes`](../go-strings-bytes-runes/SKILL.md)** — `[]byte` is a slice; the same aliasing and append rules apply to byte slices and `string`↔`[]byte`.
- **[`go-version-feature-map`](../go-version-feature-map/SKILL.md)** — `slices`/`maps`/`clear` (1.21), `maps.Keys`/`Values` iterators (1.23) gate on the `go` directive.

---

## 10. Reference Files

High-frequency slice/map anti-patterns in LLM-generated Go, each with wrong/right code and citations:

[references/common-mistakes.md](references/common-mistakes.md)

Source provenance for every claim in this skill:

[references/sources.yaml](references/sources.yaml)

