---
title: "04 — Arrays and Slices — Fixed Boxes and the Windows Onto Them"
uid: arrays-and-slices
tags: ["golang", "arrays", "type-conversion", "roadmap:golang", "make", "slices", "fundamentals", "capacity"]
excerpt: "An array is a fixed-size box whose length is part of its type; a slice is a lightweight header — pointer, length, capacity — acting as a window onto an underlying array."
date: 2026-08-13T03:28:12+0000
source: https://www.aveshina.my.id/en/blog/arrays-and-slices
---

Two types that look similar and behave differently kept tripping me until I saw the mechanism. The model that finally separated them: **an array is a fixed-size box whose length is part of its type, while a slice is a small header — a pointer plus a length and a capacity — that acts as a window onto an underlying array.** [1][2] Almost all Go code uses slices; arrays exist mostly as the backing storage slices sit on top of. Once I could see the header pointing at the box, the confusing behaviors — aliasing, capacity growth, the difference between make([]T, 5) and make([]T, 5, 10) — all snapped into place.

## Arrays — the fixed box

An array has a fixed length, and that length is part of the type [1]:

```
var a [3]int           // [3]int is a distinct type from [5]int
b := [3]int{1, 2, 3}
```

Two consequences follow from "length is part of the type." First, you cannot resize an array — [3]int is forever a three-element type. Second, arrays are **value types**: assigning one array to another, or passing one to a function, copies the entire contents [1]. That is rarely what you want, which is why raw arrays show up almost exclusively as the backing storage for slices and in memory-layout-critical code.

## Slices — the header pointing at a box

A slice is the dynamic, everyday sequence type. The key insight is that a slice is _not_ an array — it is a small **header** with three fields [2]:

```figure
<svg viewBox="0 0 720 280" xmlns="http://www.w3.org/2000/svg" class="my-6 w-full max-w-2xl" role="img" aria-label="A slice header with three fields — pointer, length, capacity — pointing into a 7-cell underlying array. The pointer lands on cell index 2. Length covers cells 2,3,4 (3 cells). Capacity covers cells 2 through 6 (5 cells). A label explains: length is what you can index, capacity is how far the window can grow before reallocation.">
  <g font-family="ui-sans-serif, system-ui, sans-serif" text-rendering="geometricPrecision">
    <!-- Slice header -->
    <rect x="30" y="30" width="220" height="100" rx="8" fill="#e0e7ff" stroke="#6366f1" stroke-width="1.5"/>
    <text x="140" y="50" font-size="12" font-weight="700" fill="#1e1b4b" text-anchor="middle">slice header</text>
    <g font-size="11" font-family="ui-monospace, monospace" fill="#1e1b4b">
      <text x="50" y="74">ptr</text><text x="240" y="74" text-anchor="end">→</text>
      <text x="50" y="96">len</text><text x="240" y="96" text-anchor="end">3</text>
      <text x="50" y="118">cap</text><text x="240" y="118" text-anchor="end">5</text>
    </g>

    <!-- pointer line -->
    <path d="M255,70 C330,70 380,70 410,150" fill="none" stroke="#64748b" stroke-width="1.5" marker-end="url(#sarrow)"/>
    <defs><marker id="sarrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto"><path d="M0,0 L10,5 L0,10 z" fill="#64748b"/></marker></defs>

    <!-- underlying array: 7 cells -->
    <g>
      <rect x="170" y="150" width="520" height="50" rx="6" fill="#f1f5f9" stroke="#cbd5e1" stroke-width="1.2"/>
      <g font-family="ui-monospace, monospace" font-size="11" fill="#475569" text-anchor="middle">
        <text x="200" y="180">0</text><text x="270" y="180">1</text>
        <text x="340" y="180" font-weight="700" fill="#1e1b4b">2</text>
        <text x="410" y="180" font-weight="700" fill="#1e1b4b">3</text>
        <text x="480" y="180" font-weight="700" fill="#1e1b4b">4</text>
        <text x="550" y="180">5</text><text x="620" y="180">6</text>
      </g>
      <!-- length window (3 cells: index 2,3,4) -->
      <rect x="310" y="148" width="210" height="54" rx="4" fill="none" stroke="#16a34a" stroke-width="2"/>
      <!-- capacity window (5 cells: index 2..6) -->
      <rect x="310" y="144" width="380" height="62" rx="4" fill="none" stroke="#ca8a04" stroke-width="2" stroke-dasharray="5 3"/>
    </g>

    <text x="415" y="225" font-size="10" fill="#16a34a" text-anchor="middle" font-weight="700">len = 3 (indexable)</text>
    <text x="500" y="245" font-size="10" fill="#ca8a04" text-anchor="middle" font-weight="700">cap = 5 (grow room before realloc)</text>
    <text x="360" y="270" font-size="10" fill="#64748b" text-anchor="middle" font-style="italic">underlying array of 7 elements</text>
  </g>
</svg>
```

- **pointer** — the address of the first element the slice can see
- **len** — how many elements are currently indexable
- **cap** — how many elements the underlying array holds from that pointer onward

Because the slice is just a header, copying a slice (assigning it, passing it to a function) copies the three-word header — cheap — and _both copies point at the same underlying array [2]. Two slices can alias the same storage, which is the source of both the power (sub-slicing is free) and the danger (modifications through one alias are visible through the other).

## Length, capacity, and growth

The len/cap distinction matters most when you append. A slice can grow up to its capacity without any reallocation — the new element just claims unused room in the underlying array. Once append would exceed cap, Go allocates a _new, larger_ underlying array, copies the old elements over, and re-points the slice header at it [3].

The growth strategy roughly doubles capacity for smaller slices and grows more conservatively for large ones. If you know the final size in advance, pre-allocate to avoid the repeated copy:

```
s := make([]int, 0, 1000)   // len 0, cap 1000 — 1000 appends with no reallocation
```

This single habit — passing a capacity hint to make — is one of the easiest performance wins in Go.

## make — initializing reference types

make is the built-in for creating and initializing slices, maps, and channels [4]. Unlike new (which returns a zeroed pointer), make returns a usable value already wired up. For slices it takes a length and an optional capacity:

```
a := make([]int, 5)         // len 5, cap 5 — five zero-value ints
b := make([]int, 5, 10)     // len 5, cap 10 — five indexable, room for five more
c := make(map[string]int)   // empty, ready-to-use map
d := make(chan int)         // unbuffered channel
```

The confusion to avoid: make([]int, 5) gives you a slice of length 5, so all five elements already exist as zeros and append would add a sixth. If you wanted an empty slice with grow room, use make([]int, 0, 5).

## Converting between arrays and slices

Two newer conversions close the loop. An array becomes a slice for free via the slice expression arr[:] or arr[start:end] — this creates a header pointing at the array's memory with no copying, so mutations through the slice affect the array [5]. Going the other way, Go 1.17+ lets you convert a slice to a fixed-size array with [N]T(slice) — this _does_ copy the data, and panics if the slice has fewer than N elements [5].

## String literals and conversions (a related aside)

Two literal forms appear alongside array/slice work. **Raw string literals** use backticks and interpret nothing — no \n, no escapes — which makes them ideal for regex, SQL, JSON templates, and multi-line text where escaping would be noisy [6]. **Interpreted string literals** use double quotes and process escape sequences like \n and \t — the everyday choice for text needing control characters [7]. And the ubiquitous conversion in this neighborhood is between strings and byte slices: []byte(s) gives you the UTF-8 bytes to manipulate, and string(bs) turns them back. That pair is how you do byte-level work on a string.

## How I use this

Three habits fall out of the header model. I almost never write [N]T directly — arrays belong to memory layouts and the rare fixed-size buffer; for any sequence I reach for a slice. When I know the eventual size, I pass a capacity to make to skip the realloc churn. And before mutating a slice that came from somewhere else, I check whether I am sharing its underlying array — if a caller might still hold a view onto the same storage, a defensive copy into a fresh slice prevents surprising aliasing bugs. The header-pointing-at-box picture is what makes all three decisions obvious.

## References

[1] The Go Authors, "Go Tour: Arrays," go.dev, 2024. [Online]. Available: [https://go.dev/tour/moretypes/6](https://go.dev/tour/moretypes/6)

[2] The Go Authors, "Go Tour: make," go.dev, 2024. [Online]. Available: [https://go.dev/tour/moretypes/13](https://go.dev/tour/moretypes/13)

[3] A. Devb, "Understanding Go's Slice Data Structure and Its Growth Pattern," Medium, 2024. [Online]. Available: [https://medium.com/@arjun.devb25/understanding-gos-slice-data-structure-and-its-growth-pattern-48fe6dd914b4](https://medium.com/@arjun.devb25/understanding-gos-slice-data-structure-and-its-growth-pattern-48fe6dd914b4)

[4] freeCodeCamp, "The new() vs make() Functions in Go," 2024. [Online]. Available: [https://www.freecodecamp.org/news/new-vs-make-functions-in-go/](https://www.freecodecamp.org/news/new-vs-make-functions-in-go/)

[5] Labex, "How to slice arrays correctly in Go," 2024. [Online]. Available: [https://labex.io/tutorials/go-how-to-slice-arrays-correctly-418936](https://labex.io/tutorials/go-how-to-slice-arrays-correctly-418936)

[6] The Go Authors, "Strings in Go," The Go Blog, 2013. [Online]. Available: [https://go.dev/blog/strings#what-is-a-string](https://go.dev/blog/strings#what-is-a-string)

[7] DigitalOcean, "An introduction to working with strings in Go," 2024. [Online]. Available: [https://www.digitalocean.com/community/tutorials/an-introduction-to-working-with-strings-in-go](https://www.digitalocean.com/community/tutorials/an-introduction-to-working-with-strings-in-go)

```quiz
Q: Why is the length part of an array's type but not a slice's type?
- Both treat length as part of the type
- An array's size is fixed at compile time, so [3]int and [5]int are distinct types; a slice has a runtime header with len/cap, so its type ignores length
correct: 1
explain: Arrays have compile-time length baked into the type. Slices are dynamic headers (ptr/len/cap) over an array, so []int is one type regardless of current length.

Q: Assigning one slice to another, or passing a slice to a function, copies…
- the entire underlying array
- only the three-word slice header; both copies point at the same backing array
correct: 1
explain: A slice is a small header. Copying it copies the header, and both headers alias the same underlying array — which is why mutations through one are visible through the other.

Q: What is the difference between make([]int, 5) and make([]int, 0, 5)?
- They are identical
- The first gives a length-5 slice of zeros (append adds a 6th); the second gives an empty slice with capacity 5 (append fills slots 0-4 before realloc)
correct: 1
explain: make's second argument is length, third is capacity. make([]int,5) creates 5 indexable zeros; make([]int,0,5) creates an empty slice with grow room for 5.

Q: What happens when append would exceed a slice's capacity?
- The element is silently dropped
- Go allocates a new, larger underlying array, copies elements over, and re-points the slice header at it
correct: 1
explain: Once len would exceed cap, append grows by allocating fresh storage. The slice header then points at the new array, and the old backing array is eventually garbage-collected.

Q: Converting a slice to a fixed-size array with [4]int(slice)…
- aliases the slice's memory with no copy
- copies the data into a new array, and panics if the slice has fewer than 4 elements
correct: 1
explain: [N]T(slice) is a Go 1.17+ conversion that copies. Because the result is a value-typed array, it needs its own storage, and it panics if the slice is too short to fill it.
```
