Go data structures
26 production-grade Go skills for Claude Code, Gemini CLI, and opencode.
npx -y skills add muratmirgun/gophers --skill go-data-structuresAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 8 stars8 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.
What its author says it does
Copied from the file, not written here
Use when choosing or operating on Go slices, maps, arrays, strings, or container/* types — including slice internals, capacity growth, preallocation, map buckets, sets via map[T]struct{}, strings.Builder vs bytes.Buffer, generic containers, and the slices/maps standard packages (Go 1.21+). Apply proactively whenever data is being collected, transformed, or copied, even if the user has not asked about allocation.
The file declares its own license as MIT. That is the author’s claim about this one file, and it is not the same thing as the license GitHub reports for the repository, which is listed with the other numbers below.
SKILL.md
8.4 KB, as published. Nobody here has run it
Go Data Structures
Pick the structure that fits the access pattern — not the most familiar one. Slices and maps are the workhorses; arrays, container types, and the slices/maps packages cover the rest. Understanding the header layout, growth costs, and copy semantics of each turns most performance questions into one-line decisions.
Core Rules
- Slices and maps are reference types — assigning copies the header, not the data. Use
slices.Clone/maps.Clonefor a true copy. - Preallocate with
make([]T, 0, n)andmake(map[K]V, n)whenever the size is known or estimable. - Always assign the result of
append— the backing array may move. - Use
slicesandmapspackages (Go 1.21+) instead of hand-rolled helpers. map[K]struct{}is the canonical set — zero-byte values, no boolean ambiguity.strings.Builderfor string building,bytes.Bufferwhen you needio.Reader/io.Writer.
Picking a Structure
What do you need?
├─ Ordered, fixed compile-time size → [N]T array
├─ Ordered, dynamic size → []T slice
│ ├─ Known size → make([]T, 0, n)
│ └─ JSON output must be [] → []T{} literal (not nil)
├─ Key/value lookup → map[K]V
│ ├─ Need a set → map[K]struct{}
│ └─ Known size → make(map[K]V, n)
├─ Priority queue / top-k → container/heap
├─ Frequent middle insertion → container/list
├─ Fixed-size rolling window → container/ring
├─ Pure string building → strings.Builder
└─ Read+write of bytes → bytes.Buffer
Slice Internals
A slice is a 3-word header: pointer, length, capacity. Multiple slices can alias the same backing array — s[1:4] shares memory with s.
Capacity Growth
The exact algorithm has changed across versions; do not rely on it. As of recent Go:
len < 256→ capacity roughly doubles.len ≥ 256→ grows by ~25%.- Each growth allocates a new backing array and copies — O(n) per growth.
Preallocation
users := make([]User, 0, len(ids)) // exact size
results := make([]Result, 0, estimated) // approximate
s = slices.Grow(s, additional) // pre-grow before bulk append (Go 1.21+)
slices Package (Go 1.21+)
| Function | Purpose |
|---|---|
Sort, SortFunc, SortStableFunc | sorting |
BinarySearch, BinarySearchFunc | sorted lookup |
Contains, Index, IndexFunc | search |
Compact, CompactFunc | dedupe adjacent equals |
Clone, Equal | safe copy / comparison |
Delete, DeleteFunc | removal preserving order |
Grow | preallocate before append |
Concat (1.22+) | concatenate slices |
Prefer these over hand-rolled loops — they're tested, generic, and use the fastest available paths.
Read references/slices-and-maps.md for capacity growth, aliasing pitfalls, and 2-D slice patterns.
nil vs Empty Slice: The JSON Trap
Both have len == 0 and cap == 0, but they encode differently:
var nilSlice []string // → JSON: null
emptySlice := []string{} // → JSON: []
API contracts almost always want []. Initialise the slice explicitly in any struct that gets marshaled to JSON, and treat nil/empty as identical when reading (use len(s) == 0).
For internal computation where nil is never marshaled, the nil slice is conventional and slightly cheaper (no allocation until first append).
Maps
Maps are hash tables with 8-entry buckets and overflow chains. They are reference types — assigning a map copies a pointer.
Preallocation
m := make(map[string]*User, len(users)) // avoids rehashing during population
The size hint is approximate (it's about bucket count), but it still saves repeated rehashing in the common case.
Sets
type Set[T comparable] map[T]struct{}
func (s Set[T]) Add(v T) { s[v] = struct{}{} }
func (s Set[T]) Has(v T) bool { _, ok := s[v]; return ok }
func (s Set[T]) Remove(v T) { delete(s, v) }
struct{} is zero bytes; the set is just the key set of the underlying map.
map[K]bool is also common but ambiguous: did false mean "explicitly excluded" or "not present"? struct{} removes the question.
maps Package (Go 1.21+)
Clone, Equal/EqualFunc, DeleteFunc; Keys, Values, Collect, Insert since 1.23 (iterators).
Read references/strings-bytes-builder.md for string-vs-bytes,
BuildervsBuffer, and rune handling.
Arrays
Fixed-size, value type, copied on assignment. Useful for compile-time-known sizes:
type Digest [32]byte
type IP4 [4]byte
cache := map[[2]int]Result{} // arrays are comparable → usable as map keys
For anything dynamic, use a slice.
container/* and Third-Party
| Package | Use case | Caveat |
|---|---|---|
container/heap | priority queue, top-K | implement the interface yourself |
container/list | LRU, frequent middle splice | poor cache locality |
container/ring | rolling window, round-robin | fixed size |
bufio | I/O with many small reads/writes | always check Flush errors |
For typed sets/queues/trees beyond the stdlib, prefer well-tested libraries (emirpasic/gods, gammazero/deque) and benchmark before optimising.
Read references/containers-and-pointers.md for heap implementation,
unsafe.Pointer's six valid patterns, andweak.Pointer[T].
Copy Semantics Cheat Sheet
| Type | Copy behaviour |
|---|---|
| primitives, arrays, structs | value (deep for contained value fields) |
| slice | header copied, backing array shared — use slices.Clone |
| map, channel | reference copied — use maps.Clone for maps |
*T, interface | address / (type, value) pair copied |
Anti-Patterns
| Anti-pattern | Why it hurts | Do this instead |
|---|---|---|
s := append(s, x) ignoring return | Backing array may move; s becomes stale | Always reassign |
var m map[K]V; m[k] = v | nil map panic | m := make(map[K]V) or map[K]V{} |
var s []T then marshal to JSON as [] | Encodes as null | s := []T{} |
make([]T, 0, 10000) "just in case" | Wasted memory | Size by actual data |
m := map[K]bool{} as a set | false is ambiguous | map[K]struct{} |
bytes.Buffer for pure string building | Extra copy in String() | strings.Builder |
| Large struct values in a map | Each lookup copies the value | map[K]*V |
Verification Checklist
- Every
make([]T, ...)andmake(map[K]V, ...)has a capacity hint when the size is known. - Every
appendreassigns its result. - Slices marshaled to JSON are initialised as
[]T{}, notvar s []T. - All "sets" use
map[K]struct{}(or a genericSet[T]wrapper). - No
bytes.Bufferused purely forString()output. - No
*sync.Mutexcopied via struct assignment (go vet copylocks). -
slices.Clone/maps.Cloneused when handing data to callers that may mutate.
References
- references/slices-and-maps.md — internals, growth, aliasing,
slices/mapspackages - references/strings-bytes-builder.md —
strings.Builder,bytes.Buffer, rune handling - references/containers-and-pointers.md —
container/heap, generic wrappers,unsafe.Pointer,weak.Pointer