skip to content

Slices, Maps and Structs

The aggregate value types and what really happens when you copy, share or grow one. Nearly every Go bug that survives review starts here, as a subslice quietly aliasing its parent.

part ofGo (Golang)overview, primer and where to startread it →
on this pageshow

explore

questions

24

In Go, what is the difference between an array `[5]int` and a slice `[]int`?

level: juniorimportance: must knowfreq 88%

answer

  1. think about what assignment actually copies
  2. one of the two has a fixed size
  3. [3]int and [4]int are different types
  4. the other is a view with a capacity

basics

~20 s

An array's length is part of its type, and assigning or passing one copies every element. A slice is a resizable view onto a backing array, carrying a length and a capacity; copying a slice shares that array.

solid answer

~50 s

In Go, `[5]int` is an array: the length 5 belongs to the type, so `[5]int` and `[6]int` are different types, `len` on it is a compile-time constant, and assigning it or passing it to a function copies all five elements. `[]int` is a slice: a value that refers to a backing array and carries a length and a capacity, so its length is a run-time property and can differ from one value to the next. Copying a slice value copies that reference, which means two slices can see each other's writes to the shared elements. Slicing an addressable array with `a[:]` produces a slice over the array's own storage, which is the usual way to hand an array to code that expects a slice. Almost all Go APIs take slices; arrays show up where a size is fixed and known, such as a `[32]byte` digest.

code

go · 9 lines
go
a := [3]int{1, 2, 3}
b := a // copies all three elements
b[0] = 99
// a[0] is still 1

s := []int{1, 2, 3}
t := s // copies the slice value, not the elements
t[0] = 99
// s[0] is now 99

go deeper

for a junior

Be ready to state the two headline facts without hesitating: the length is part of an array's type, and assigning or passing an array copies every element while assigning a slice shares the elements. Know that a[:] turns an array into a slice.

for a middle

Expect to explain the mechanics: a slice value carries a length and a capacity alongside its reference to a backing array, an array's len is a compile-time constant, and slicing an array requires the array to be addressable.

for a senior

Show the production instinct: spot a large array passed by value in a hot path, and know when a fixed size in the type is a real guarantee rather than an inconvenience. Be able to explain aliasing consequences to a reviewer in one sentence.

for a principal

Frame it as an API question. Fixed-size arrays put a length guarantee in the type at the cost of copying and conversion friction at every boundary; slices are the lingua franca of Go APIs. Decide which your package exposes before other teams depend on it.

## Two different types, not two spellings of one Go's `[5]int` and `[]int` look similar and behave almost nothing alike. Understanding the difference is the first real hurdle for anyone arriving from a language where "array" means one thing. ### An array is a value with its length in its type An array type is written `[N]T`, where `N` is a constant expression. That `N` is part of the type's identity: `[3]int` and `[4]int` are two distinct types, and no assignment or argument passing converts between them. Because the length is fixed in the type, `len(a)` for an array `a` is a constant the compiler folds away, and there is no way for an array's length to change at run time. An array is also an ordinary value, in exactly the way an `int` or a struct is. Assignment copies it: ```go var a [3]int b := a b[0] = 99 // a[0] is still 0 ``` The same is true at a call boundary: `func f(x [3]int)` receives a copy of all three elements, and whatever `f` writes into `x` is invisible to the caller. If you want the callee to write through to the caller's array, you pass `*[3]int` or, far more commonly, a slice. The storage for an array lives wherever the array itself lives. A local array sits in the function's frame unless the compiler decides it must escape; an array field inside a struct sits inline in that struct, with no separate allocation and no pointer to follow. ### A slice is a view onto a backing array A slice type is written `[]T`, with no length. A slice value refers to a contiguous run of elements in some backing array and carries two numbers with it: a **length** (`len(s)`, how many elements you may index) and a **capacity** (`cap(s)`, how many elements are available in the backing array from the slice's start). Different slice values of the same type routinely have different lengths, so the length is data, not type. Copying a slice value copies the reference along with the two numbers; it does not copy elements: ```go s := []int{1, 2, 3} t := s t[0] = 99 // s[0] is now 99 ``` That is why passing a slice to a function is cheap regardless of how many elements it holds, and why a function that writes `s[0] = x` is visible to its caller. Growing a slice is a separate matter with its own rules; what matters here is that an array can never grow, while a slice's length is a run-time value. ### Moving between them Slicing an array produces a slice over that array's storage: given `var arr [4]byte`, the expression `arr[:]` is a `[]byte` with length 4 and capacity 4, sharing `arr`'s bytes. A later write through the slice changes `arr`. The array being sliced must be addressable, which is why you can slice a variable but not the array returned directly by a function call. In the other direction, a slice can be converted to an array (`[4]byte(b)`, which copies) or to an array pointer (`(*[4]byte)(b)`, which aliases), and both panic if the slice is too short. ### Why the standard library almost always uses slices A function that takes `[]byte` accepts input of any length, accepts a sub-range of a larger buffer without copying, and can be handed the storage of an array with `[:]`. A function that takes `[32]byte` accepts exactly 32 bytes and copies them on every call. So slices are the general-purpose sequence type, and arrays appear where the size is genuinely fixed by a specification: a 32-byte digest, a 16-byte network address, a fixed-size frame read from a device. ### The mistakes to avoid - Treating an array parameter as a reference, as it would be in C or Java, and being surprised that the caller sees nothing. - Treating a slice as a container that owns its elements, and being surprised that two slice variables affect each other. - Assuming `[3]int` will convert to `[4]int` or to `[]int` implicitly; neither happens, and the slice case needs an explicit `a[:]`. - Passing a large array such as `[4096]byte` by value in a hot path and paying a 4 KB copy per call without noticing.

  • If passing an array copies it, how do you let a function modify the caller's array?
    Two ways. Pass a pointer, `func f(a *[3]int)`, and index through it as `a[0]` — Go dereferences the array pointer for you. Or, far more idiomatically, pass a slice over the array's storage: `f(arr[:])` with `func f(s []int)`. The slice refers to the same elements, so the callee's writes land in the caller's array.
  • What does `len` report for an array versus a slice, and when is it known?
    For an array the value comes from the type, so `len(a)` on a `[5]int` is the constant 5 — usable anywhere a constant is, including an array length itself. For a slice it is run-time data stored with the slice value, and it can differ between two values of the same slice type or change as the program reslices.
  • Why do standard-library functions take `[]byte` rather than a fixed-size byte array?
    A `[]byte` parameter accepts any length, accepts a sub-range of a bigger buffer with no copy, and lets the callee write through to the caller's storage — all of which reading and writing APIs need. A `[N]byte` parameter would fix the length in the signature and copy N bytes on every call.

saying these in an interview costs you the question

  • Says a Go array is just a slice with a fixed size
  • Thinks passing an array to a function passes a reference
  • Believes [3]int and [4]int are the same type
  • Cannot say what a slice's capacity means
  • Claims arr[:] copies the array's elements
open as a page

What does Go copy when you assign a struct that has slice and map fields?

level: juniorimportance: must knowfreq 78%

basics

~20 s

Assignment copies a struct field by field. Number, string, bool and array fields become independent, but a slice, map or pointer field copies only the reference, so both structs still read and write the same underlying data.

open as a page

In Go, what is the difference between a keyed struct literal and a positional one?

level: juniorimportance: must knowfreq 68%

basics

~20 s

A keyed struct literal names each field and may omit fields, so order does not matter. A positional literal must supply every field in declaration order, so reordering fields silently changes what each value means.

open as a page

In Go, how do you distinguish a missing map key from one whose stored value is zero?

level: juniorimportance: must knowfreq 78%

basics

~20 s

Use the two-value comma-ok form: v, ok := m[key]. ok is true only when the key is present. A one-value lookup returns the value type's zero value for a missing key, so 0, an empty string or false cannot tell you which happened.

open as a page

Go passes every argument by value — how do you write a function that modifies the caller's struct?

level: juniorimportance: must knowfreq 88%

basics

~20 s

Pass a pointer. A function parameter of type Player receives a copy, so writes to it die with the call. Declare the parameter as *Player, call it with &pl, and write p.HP = 40 — Go dereferences the selector for you.

open as a page

In Go, what is the difference between a nil slice and an empty non-nil slice?

level: juniorimportance: must knowfreq 70%

basics

~20 s

A nil slice has no backing array; an empty non-nil slice points at one holding zero elements. Both have length 0 and both accept append. They differ when compared to nil, and in JSON: nil marshals to null, empty to [].

open as a page

In Go, what is the difference between make([]int, 3) and new([]int)?

level: middleimportance: must knowfreq 72%

basics

~20 s

make([]int, 3) builds an initialised slice of three zeroed ints and returns the slice itself. new([]int) allocates only a zeroed slice header and returns a *[]int that points at a nil slice of length 0.

open as a page

Why does writing to a nil Go map panic when reading from one works fine?

level: middleimportance: must knowfreq 68%

basics

~20 s

A nil map has no hash table behind it. Reads, len, range and delete are defined to behave as if it were empty, but a write has nowhere to store the entry, so it panics with "assignment to entry in nil map". Create the map with make or a map literal.

open as a page

When does append write into a Go slice's existing backing array instead of a new one?

level: middleimportance: must knowfreq 78%

basics

~20 s

append writes in place when the new length still fits the slice's capacity. Otherwise it allocates a fresh array and copies into it. You cannot tell which happened at the call site, so always use append's return value.

open as a page

In Go, what does `[32]byte(b)` do when `b` is a `[]byte` of length 17?

level: middleimportance: should knowfreq 35%

basics

~20 s

It panics at run time. Converting a slice to an array is a length-checked copy: the array's length must be no greater than the slice's length, and 17 is short of 32, so the conversion fails with a run-time panic rather than a compile error.

open as a page

Why is copying a Go struct that contains a sync.Mutex a bug?

level: middleimportance: should knowfreq 46%

basics

~20 s

The mutex is copied as plain data, lock state included, so the copy is a second independent lock. Goroutines holding different copies enter the same critical section together. go vet's copylocks check reports it at build time.

open as a page

How do you deep copy a Go struct that holds a map of slices?

level: middleimportance: should knowfreq 58%

basics

~20 s

Go has no built-in deep copy. Write a Clone method: copy the struct, then allocate a new map, and for every entry clone the slice value before inserting it. Recurse until nothing mutable is shared.

open as a page

In Go, what does the composite literal &Point{X: 1} evaluate to?

level: middleimportance: should knowfreq 46%

basics

~20 s

&Point{X: 1} allocates a new Point with X set to 1 and the other fields zeroed, and evaluates to a *Point. Go lets you take the address of a composite literal directly, so no temporary variable is needed.

open as a page

Why can't you assign to a struct field of a Go map element, as in m[k].Count++?

level: middleimportance: should knowfreq 50%

basics

~20 s

Map elements are not addressable in Go, so you cannot take &m[k], assign to a field of an element, or call a pointer-receiver method on one. Read the value into a variable, modify that copy, and assign the whole element back — or make the value type a pointer.

open as a page

Why does `for _, p := range players` over a []Player silently lose writes to p?

level: middleimportance: should knowfreq 66%

basics

~20 s

Because range assigns a copy of each element to p. Writing p.HP changes that copy, not the slice element. Fix it by writing through the index, players[i].HP, or by holding a []*Player so the copied value is a pointer.

open as a page

If Go copies every argument, why can a function mutate the caller's map and slice elements?

level: middleimportance: should knowfreq 58%

basics

~20 s

Because what gets copied contains a pointer. A slice value is a three-word header (array pointer, length, capacity) and a map value is a handle to one hash table, so the copy still refers to the same underlying data. Writes through it are shared; changes to the header itself are not.

open as a page

When would you declare a `[32]byte` parameter in a Go API instead of `[]byte`?

level: seniorimportance: should knowfreq 40%

basics

~20 s

Use a fixed-size array parameter when the size is part of the contract and you want the compiler to enforce it: a digest, a key, a fixed frame. It also gives the callee an independent copy. Pay for it in copying and in conversions at every slice boundary.

open as a page

While ranging over a Go map, what happens to entries you delete or add mid-loop?

level: seniorimportance: should knowfreq 42%

basics

~20 s

Deleting is safe and defined: an entry removed before the loop reaches it is never produced, and removing the current key is fine. Adding is not defined: a key created during the loop may or may not be produced, and the choice can differ per entry and per run.

open as a page

A log shipper keeps each parsed line as a subslice of the 1 MB buffer it was read from, and heap use climbs steadily. Why?

level: seniorimportance: should knowfreq 46%

basics

~20 s

A slice keeps its whole backing array alive, not just the elements it exposes. Each retained line pins its entire 1 MB buffer, so the heap grows with buffers referenced. Copy kept bytes into right-sized slices.

open as a page

In Go, how does `[3][4]int` differ from `[][]int` for a two-dimensional grid?

level: middleimportance: nice to knowfreq 26%

basics

~20 s

[3][4]int is one value holding 12 ints contiguously, with both dimensions fixed by the type and the whole thing copied on assignment. [][]int is a slice of independent row slices: rows are allocated one at a time, may have different lengths, and are shared rather than copied.

open as a page

What does the third index in Go's full slice expression s[a:b:c] control, and why use it?

level: middleimportance: nice to knowfreq 34%

basics

~20 s

The third index sets capacity: s[a:b:c] has length b-a and capacity c-a. Capping capacity at the length forces any later append to allocate a fresh array, so it cannot overwrite elements the source still uses.

open as a page

A caller mutates Config.Headers after passing the Config by value into your library's New - why does that still change your client's behaviour, and what do you change?

level: seniorimportance: nice to knowfreq 34%

basics

~20 s

Passing the Config by value copies only the map's reference, so the caller and the library share one map. Clone the reference-shaped fields inside New, document which fields the library takes ownership of, and keep deliberately shared dependencies uncloned.

open as a page

Why does clear(s) before s = s[:0] matter when reusing a []*Record buffer across batches?

level: seniorimportance: nice to knowfreq 28%

basics

~10 s

Reslicing to s[:0] only moves the length to zero; the pointers stay in the backing array, so the collector keeps every Record alive. clear(s) zeroes those elements, so it must run before the truncation.

open as a page

A tick goroutine writes Player fields through a *Player while another does snap := *p — is that safe?

level: seniorimportance: nice to knowfreq 34%

basics

~20 s

No. Copying a struct is a field-by-field read, not an atomic operation, so the snapshot can mix new and old fields while the writer is mid-update. It is a data race, the race detector reports it, and the fix is a lock or publishing whole immutable snapshots.

open as a page