Go has two collection types that look almost identical in syntax but behave completely differently. When you ask AI to work with collections in Go, it will almost always use slices, and that's correct. But understanding why slices exist, how they relate to arrays, and where the shared-memory gotchas hide is what separates someone who can debug AI output from someone who copies it blindly.
Arrays: fixed size, rarely used directly
An array in Go has its size baked into the type. [5]int is not the same type as [3]int. You cannot pass one where the other is expected. This makes arrays rigid and impractical for most real code.
When you ask AI to generate Go code, it almost never produces raw arrays. But you need to recognize them because slices are built on top of arrays.
var scores [5]int // five zeros
scores[0] = 95
temps := [3]float64{72.5, 68.0, 75.3} // initialized
auto := [...]int{10, 20, 30} // compiler counts: [3]int| Array feature | What it means for you |
|---|---|
| Size is part of the type | [3]int and [5]int cannot be assigned to each other |
| Value type (copied on assignment) | Passing a large array to a function copies every element |
| Fixed at compile time | Cannot grow or shrink, no append() |
| Zero-valued by default | Unset elements are 0, "", false, etc. |
[...]int{...} (array with inferred size) when it should generate []int{...} (slice literal). The difference is one pair of dots, but arrays can't be appended to, resized, or passed to functions expecting slices. If you see [...] in generated code and the collection needs to grow, change it to [].Slices: what you actually use
A slice is a lightweight descriptor that points to a segment of an underlying array. It has three components: a pointer to the backing array, a length, and a capacity.
// Slice literal - no size in brackets
fruits := []string{"apple", "banana", "cherry"}
// make() with length
numbers := make([]int, 5) // [0 0 0 0 0], cap = 5
// make() with length AND capacity
buffer := make([]byte, 0, 1024) // empty, but room for 1024 bytes
// Slicing an array creates a slice view
arr := [5]int{10, 20, 30, 40, 50}
view := arr[1:4] // []int{20, 30, 40}Length vs capacity
This distinction matters when evaluating AI-generated code that builds up slices in loops.
| Property | What it is | Function |
|---|---|---|
| Length | Elements you can currently access | len(s) |
| Capacity | Total space before reallocation | cap(s) |
data := make([]int, 3, 10)
fmt.Println(len(data)) // 3
fmt.Println(cap(data)) // 10
// data[5] would panic - length is 3, even though capacity is 10make([]int, n) when it means make([]int, 0, n). The first creates a slice of n zeros and then appends on top of those zeros. The second creates an empty slice with room for n elements. If you see AI generate make([]T, n) followed by append() calls, the result will have n leading zero values you didn't want.append() and the reallocation trap
append() adds elements and returns a new slice header. If the backing array has capacity, it writes in place. If not, Go allocates a new, larger array and copies everything over.
s := make([]int, 0, 3)
s = append(s, 1, 2, 3) // fits in capacity - same backing array
s = append(s, 4) // exceeds capacity - NEW backing array allocatedThis matters because two slices can share the same backing array, until one of them triggers a reallocation:
original := []int{1, 2, 3, 4, 5}
sub := original[1:3] // [2, 3] - shares backing array with original
sub[0] = 999
fmt.Println(original) // [1, 999, 3, 4, 5] - original changed!
// But if sub triggers reallocation...
sub = append(sub, 10, 20, 30, 40) // exceeds capacity, new array
sub[0] = 0
fmt.Println(original) // [1, 999, 3, 4, 5] - original NOT affectedThe behavior changes depending on whether append() reallocated. This is the single most common source of subtle slice bugs.
The decision table for append safety
| Situation | Shared backing array? | append() safe? |
|---|---|---|
s := make([]int, 0, n) then only append | No other references | Safe |
sub := original[i:j] then modify sub elements | Yes, modifies original too | Dangerous |
sub := original[i:j] then append within capacity | Yes, overwrites original's later elements | Very dangerous |
sub := original[i:j] then append beyond capacity | No, reallocation breaks the link | Safe but confusing |
sub := data[i:j] followed by append(sub, ...), check whether the append could overwrite elements in data that are still needed. The fix is usually copy() or the full slice expression data[i:j:j] which limits capacity.The full slice expression: s[low:high:max]
Go has a three-indexWhat is index?A data structure the database maintains alongside a table so it can find rows by specific columns quickly instead of scanning everything. slice expression that limits the capacity of the resulting slice:
original := []int{1, 2, 3, 4, 5}
safe := original[1:3:3] // len=2, cap=2 (not cap=4!)
// Now append to safe will ALWAYS allocate a new array
safe = append(safe, 99)
fmt.Println(original) // [1 2 3 4 5] - untouchedThis is the defensive pattern you should look for, or add, when reviewing AI-generated slice code.
Copying slices safely
copy() is Go's built-in for creating independent slices:
original := []int{1, 2, 3, 4, 5}
clone := make([]int, len(original))
copied := copy(clone, original) // returns number of elements copied
clone[0] = 999
fmt.Println(original[0]) // 1 - independentSince Go 1.21, you can also use slices.Clone() from the standard libraryWhat is standard library?A collection of ready-made tools that come built into a language - no install required. Covers common tasks like reading files or making web requests., which is cleaner:
import "slices"
clone := slices.Clone(original)Pre-allocation: the performance pattern AI misses
When AI generates a loop that builds a slice, it rarely pre-allocates:
// AI typically generates this:
var results []string
for _, item := range items {
results = append(results, transform(item))
}
// Better - pre-allocate when you know the size:
results := make([]string, 0, len(items))
for _, item := range items {
results = append(results, transform(item))
}The second version avoids repeated reallocations. For small slices it doesn't matter. For thousands of elements, the difference is measurable.
Deleting elements from slices
There's no built-in delete() for slices. AI will generate the classic append trick:
// Remove element at index i (does NOT preserve order)
s[i] = s[len(s)-1]
s = s[:len(s)-1]
// Remove element at index i (preserves order)
s = append(s[:i], s[i+1:]...)i, which is O(n). The swap-with-last version is O(1) but changes the order. AI almost always generates the order-preserving version even when order doesn't matter.Nil slice vs empty slice
var nilSlice []int // nil, len=0, cap=0
emptySlice := []int{} // not nil, len=0, cap=0
madeSlice := make([]int, 0) // not nil, len=0, cap=0All three behave identically with len(), cap(), append(), and range. The difference matters for JSONWhat is json?A text format for exchanging data between systems. It uses key-value pairs and arrays, and every programming language can read and write it. serializationWhat is serialization?Converting data from a program's internal format into a string or byte sequence that can be stored or sent over a network.: nil marshals to null, empty marshals to []. AI-generated APIWhat is api?A set of rules that lets one program talk to another, usually over the internet, by sending requests and getting responses. code gets this wrong constantly.