---
title: 'Arrays and Slices'
source: 'https://academia.sh/en/courses/go-fundamentals/arrays-and-slices'
course: 'Go Fundamentals'
language: en
updated: '2026-09-13T05:08:44+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Shows that array and slice assignment produce two different results in the same syntax'
    - "Reads a slice's length-capacity pair and states that growth behavior is an implementation detail"
    - 'Measures with an append call when a slice header splits away from its copy'
---

# Arrays and Slices

The same assignment line produces a copy in an array and sharing in a slice: an array's length is part of its type, and all elements are copied when it is assigned, while assigning a slice copies only the header and shares the backing array with the source. The limiting measure shows that a slice is also a value: sharing is cut off once an append call exceeds capacity, because the copied header now points to a separate backing array.

The Variadic Arguments lesson measured that a parameter given no argument at all is a `nil`
slice — one of the zero value's most visible consequences. This lesson returns to that same
type itself: how a slice, a map and a struct answer the unwritten, and a question never asked
before — when you assign a value to a variable, does a copy result, or sharing?

The Data Structures course established the array and the list as abstract data types and
compared implementation choices by complexity; that comparison is not repeated here. What is
measured here is the **observable** behavior of Go's own two types — array and slice — during
assignment: the same `=` sign does two different things depending on the type of the value on
the right.

Array, slice, map and struct are referred to in this course by the common name **composite
type**, because all four are built from other types: an array from `int`s, a struct from named
fields. The difference among composite types lies in how much gets copied during assignment —
and this lesson begins with the two most basic members of that difference, the array and the
slice.

## Array: Fixed Length and Copied Assignment

In Go, an array's length is part of its type; `[4]int` and `[3]int` are two separate types,
just as `int` and `string` are two separate types. This is not a loose description like "an
array of four elements," it is exact information the compiler reads from the syntax.

**CT1.** — That a source directly assigning between two array types of different length fails
to compile is not run here with a compiler call; the measurement instead shows the same
information indirectly with the `%T` formatting verb — the printed type name itself carries
the length.

```go
// composite.go — array assignment copies, the type name carries the length
package main

import "fmt"

func main() {
	sourceArray := [4]int{10, 20, 30, 40}
	copyArray := sourceArray
	copyArray[0] = 99
	fmt.Println("type         :", fmt.Sprintf("%T", sourceArray))
	fmt.Println("sourceArray  :", sourceArray)
	fmt.Println("copyArray    :", copyArray)
}
```

```
type         : [4]int
sourceArray  : [10 20 30 40]
copyArray    : [99 20 30 40]
```

Had `var copyArray [3]int` been written right above the line `copyArray := sourceArray` in the
same source, compilation would stop right there; the assignment would be rejected because
`[3]int` and `[4]int` are two types that do not substitute for each other. This lesson does
not show this with a separate compiler call, because the Go's Design Goals lesson already
measured the rejection of implicit type conversion; the difference here is only that length,
too, is part of type identity.

The printed type is `[4]int`, not just `int` — the length stands inside the type name. After
the line `copyArray := sourceArray`, changing the first element affects only `copyArray`;
`sourceArray` stays unchanged. When an array is assigned, **all** four integers are copied to
a new variable — the syntax is a single equals sign, but underneath it there are four separate
copy operations.

This copying works not only in variable-to-variable assignment, but also when an argument is
passed to a function; parameters are assignments too. Another consequence of length being part
of the type follows from this: two arrays, as long as their element types are comparable, can
be compared directly with `==` — because what gets compared is not a pointer, it is the
array's **content itself**. The same property also explains why an array can be used as a map
key; as will be seen in the Maps lesson, a map's key type must be comparable, and an array
satisfies this condition naturally. The same rule works downward too: a struct with a slice
field also becomes non-comparable, because a struct's comparability depends on its fields'
comparability — this will come up again in the Structs lesson.

## Slice: Length, Capacity and the Shared Backing Array

A slice is not an array; the slice itself is a small structure made of three parts: a pointer
to a backing array, the current length, and the capacity. The notation
`[]int{10, 20, 30, 40}` sets up all three at once — it produces a four-element backing array
behind it, and the slice points to that backing array.

```go
// sharing.go — slice assignment copies the header, the backing array is shared
package main

import "fmt"

func main() {
	sourceSlice := []int{10, 20, 30, 40}
	shareSlice := sourceSlice
	shareSlice[0] = 99
	fmt.Println("sourceSlice  :", sourceSlice)
	fmt.Println("shareSlice   :", shareSlice)
	fmt.Println("length       :", len(sourceSlice), " capacity:", cap(sourceSlice))
}
```

```
sourceSlice  : [99 20 30 40]
shareSlice   : [99 20 30 40]
length       : 4  capacity: 4
```

The same line — `shareSlice := sourceSlice` — does not produce a copy the way it does with an
array. When `shareSlice[0] = 99` is written, `sourceSlice` changes too, because both point to
the same backing array; what gets assigned is not the backing array itself, it is the small
header carrying the pointer to it. `length` and `capacity` come out equal here because the
slice literal was built with exactly four elements; when a slice is derived by slicing, the
two can diverge — that distinction is the Slice Operations lesson's subject.

The header being small and fixed in size has a direct consequence: the cost of copying a slice
is independent of how many elements the backing array holds. Assigning a ten-element slice and
assigning a ten-million-element slice both copy the same three fields; what gets copied is
never the elements themselves. The cost of assigning an array, by contrast, grows with its
length, because every element is copied one by one.

This distinction also determines where each type gets used. Small data whose length is known
ahead of the code and will never change — a four-byte IPv4 address, a fixed-length
checksum — is defined as an array; writing the size into the type signature is a way of
telling the compiler that size will never change. Everything whose length will change during
the run — the number of lines read, an accumulating result set — is kept as a slice, because a
slice's length is a field of the value, not of the type.

A slice not being comparable with `==` traces back to the same root. If a slice comparison
were to try to answer "is the content the same," the compiler would have to walk the backing
array from start to end on every comparison; instead, the language allows a slice to be
compared only with `nil`, not with another slice. The same `==` operator compiles for an array
and does not compile for a slice — because an array is the content it carries, while a slice
is a pointer leading to it.

**CT2.** — This distinction is run here with a compiler call on the source; the two printed
lines carry only the class "compiled" or "did not compile," the compiler's error text is not
read.

```go
// comparison.go — an array compares with ==, a slice cannot
package main

import (
	"fmt"
	"os"
	"os/exec"
	"path/filepath"
)

func compiles(source string) bool {
	dir, err := os.MkdirTemp("", "trial")
	if err != nil {
		return false
	}
	defer os.RemoveAll(dir)
	os.WriteFile(filepath.Join(dir, "go.mod"), []byte("module trial\n\ngo 1.24\n"), 0o644)
	os.WriteFile(filepath.Join(dir, "code.go"), []byte(source), 0o644)
	cmd := exec.Command("go", "build", "./...")
	cmd.Dir = dir
	cmd.Env = append(os.Environ(), "GOTOOLCHAIN=local")
	return cmd.Run() == nil
}

func main() {
	arrayEqual := `package trial
	func F() bool {
		a := [3]int{1, 2, 3}
		b := [3]int{1, 2, 3}
		return a == b
	}`
	sliceEqual := `package trial
	func F() bool {
		a := []int{1, 2, 3}
		b := []int{1, 2, 3}
		return a == b
	}`
	fmt.Println("two arrays compare with ==:", compiles(arrayEqual))
	fmt.Println("two slices compare with ==:", compiles(sliceEqual))
}
```

```
two arrays compare with ==: true
two slices compare with ==: false
```

## The Same Rule Applies When Passing to a Function

Parameter passing is also an assignment, and the rule above applies here in exactly the same
way: an array parameter gets its own copy; a slice parameter shares the same header — and
therefore the same backing array. This is proof that Go makes no distinction at all between an
assignment between two variables and a function call: parameter binding follows the same rule
as the `=` sign.

```go
// passing.go — passing to a function is an assignment too
package main

import "fmt"

func changeArray(a [4]int) {
	a[0] = 999
}

func changeSliceElement(s []int) {
	s[0] = 999
}

func reassignSlice(s []int) {
	s = append(s, 777, 777, 777, 777, 777) // exceeds capacity, moves to a new backing array
}

func main() {
	array := [4]int{1, 2, 3, 4}
	changeArray(array)
	fmt.Println("array passed to function :", array)

	slice := []int{1, 2, 3, 4}
	changeSliceElement(slice)
	fmt.Println("slice element write      :", slice)

	slice2 := []int{1, 2, 3, 4}
	reassignSlice(slice2)
	fmt.Println("slice reassignment       :", slice2)
}
```

```
array passed to function : [1 2 3 4]
slice element write      : [999 2 3 4]
slice reassignment       : [1 2 3 4]
```

The first line is as expected: `changeArray` operates on its own copy; the caller's array does
not change. The second line confirms sharing: `changeSliceElement` writes to the backing array
its parameter points to, and this write is reflected back to the caller. The third line shows
a subtle point: when `reassignSlice` grows the parameter with `append` and **reassigns** it,
only its own local copy's header changes; the caller's `slice2` header still points to the old
backing array, and that change is lost when the function returns. Passing a slice to a
function shares the backing array but does not share the header — this is the function-call
counterpart of the header split in the Limiting Measure section.

The standard library's own `append` function returns a result for this very reason and expects
the caller to reassign it; even if the function grew the slice it received as a parameter and
kept it internally, this would not affect the caller's variable. The behavior of the
`reassignSlice` function in the third line is direct proof of why `append` is called in the
form `s = append(s, x)`: code that forgets to reassign the result silently stays with the old
slice once capacity is exceeded.

## Limiting Measure: When a Slice Header Splits Away

What a slice shares is the backing array, not the header itself. The header is also a value
and is copied when assigned — its three fields (pointer, length, capacity) together. This
difference becomes visible the moment two variables stop pointing to the same backing array.

**CT3.** — The capacity here is not explicitly requested with `make`; `[]int{1, 2, 3}` is a
slice literal, and the compiler sets the capacity equal to three elements. The measurement
does not assume this capacity is fixed, it reads it from the run with `cap`.

```go
// header.go — when a slice header splits away from its copy
package main

import "fmt"

func main() {
	base := []int{1, 2, 3}
	share := base
	share[0] = 100
	fmt.Println("after element write   base:", base, " share:", share)

	previousCapacity := cap(base)
	base = append(base, 4)
	share[1] = 200
	fmt.Println("after append          base:", base, " share:", share)
	fmt.Println("capacity (this run) previous=", previousCapacity, " next=", cap(base))
}
```

```
after element write   base: [100 2 3]  share: [100 2 3]
after append          base: [100 2 3 4]  share: [100 200 3]
capacity (this run) previous= 3  next= 6
```

In the first line, `share[0] = 100` changes both `base` and `share` — both still point to the
same backing array, just as in the previous section. Then the call `base = append(base, 4)`
exceeds the capacity (there is no room for a fourth element in a three-element backing array);
in this case the language's runtime allocates a **new** backing array, copies the old data
into it, and updates `base`'s header to point to that new backing array. `share`'s header
stays unaware of this update — it still points to the old backing array. This is why
`share[1] = 200` changes only `share`; it leaves no trace on `base`.

The unpredictability of this transition carries the real risk. Someone looking at the source
code cannot tell, from the line itself alone, whether `share[1] = 200` will affect `base` —
the answer depends on how many times `share` and `base` crossed a shared `append` boundary
before that line is reached. When two slices share the same backing array and one is grown
while the other is not, the sharing between them ends without leaving any trace in the text.

**CT4.** — The two printed capacity numbers (3 and 6) are this run's observation, not the
language's promise. The Go specification guarantees that `append` will allocate a new backing
array when capacity is exceeded, but it does not specify **how much** the new capacity will
grow by; the growth rate is an implementation detail and can change between compiler versions.
Code should not read this line as a constant — it should read only the information "capacity
was exceeded, a new backing array arrived."

What gets copied here is now fully clear: `base` and `share` were never the same variable,
both had their own header from the start. The sharing came from the two headers pointing to
the **same backing array**; once `append` changes the backing array, the sharing ends. The
slice itself is a value — just as the array is a value — but the value it carries is not the
backing array's data, it is a pointer leading to it.

The practical counterpart of this is: if a function adds elements to the slice it receives
with `append` and wants that growth reflected back to the caller, it must **return** the grown
slice — just as `append`'s own signature does. Growing the slice variable that arrives as a
parameter, on its own, only updates that local variable; the caller's header is not affected
by this. The difference between array and slice thus comes down to a single sentence: an array
is a value and is copied entirely, a slice is also a value, but what it carries is a small
header leading to a shared backing array — neither is "passing by reference," both are passing
by value with different content. What this distinction has to do with pointers — when taking a
variable's address is required, when it is unnecessary — is measured separately in the
Pointers lesson; what is established here is only that a slice's own header is nothing more
than a unit of copying.

## Summary

- An array's length is part of its type (`[4]int`); when an array is assigned, all elements
  are copied and the two variables become independent of each other.
- A slice is a three-field header: a pointer to the backing array, the length, the capacity.
  When a slice is assigned, this header is copied but the backing array is not; the two
  variables share the same backing array.
- A change written to the shared backing array is visible from both variables, because both
  point to the same memory.
- When an `append` call exceeds capacity, a new backing array is allocated, and only the
  variable holding the call's result moves to that new backing array; sharing is cut off here.
- Capacity's growth rate (such as from 3 to 6) is an implementation detail; the language's
  only promise is the statement "when capacity is exceeded, a new backing array is allocated."

## Next Step

In this lesson, sharing was seen only when two variables directly carried the same slice —
through an assignment like `shareSlice := sourceSlice`, or by being passed as a parameter to a
function. But the same backing array is also shared when a slice is derived from another slice
by **slicing** — and in that case, when the sharing continues and when it cuts off depends not
on the written code, but on the derived slice's capacity. The next lesson measures this
dependency: the same `append` call, with the same source text, gives two different results at
two different capacities.
