Course outline

Go / Basic: Values and Program Contracts / Lesson 4

Arrays, Slices, and Text Boundaries

Track length, capacity, backing storage, and text units instead of treating every sequence as an independent copy.

About 5 min readContent reviewed
On this page

Lesson goals

  • Distinguish array values from slice descriptors.
  • Predict aliasing and capacity-limited append behavior.
  • Choose safe indexing and text iteration units.

A slice describes a view of an array

An array’s length belongs to its type, and assigning an array copies its elements. A slice is a value describing a segment of a backing array, with length and capacity. Assigning the slice copies that descriptor, not the elements. Several slices can therefore expose overlapping storage even when their local variable names are different.

Length controls valid indexing: zero through len(s) minus one. Capacity describes how far a slice may be extended by reslicing from its current start. It does not authorize indexing beyond length. A nil slice has zero length and capacity, supports range and append, and is different from a guaranteed non-nil empty slice.

Make independence explicit

The first program creates a view sharing base, then makes an explicit independent element copy. Changing view[0] affects base because that element is shared. Changing independent[1] does not affect view because append to the initially nil destination allocated storage for the copied values. The fixed int elements do not contain further reference-bearing fields.

A shallow element copy is not a recursive deep copy. For [][]int or structs containing maps, copying the outer elements can still leave nested data shared. Document whether a function returns a borrowed view or owned independent data, and test the aliasing behavior that callers are permitted to rely on.

go A shared view and a deliberate copy
package main

import "fmt"

func main() {
	base := []int{1, 2, 3}
	view := base[:2]
	view[0] = 9
	independent := append([]int(nil), view...)
	independent[1] = 8
	fmt.Println(base, view, independent)
}
Expected output
[9 2 3] [9 2] [9 8]

The first change reaches base; the change to independently copied storage does not.

Keep the returned slice

append returns an updated slice. If capacity suffices, it can reuse the existing backing array; otherwise it allocates a new sufficiently large array and copies the existing elements. The exact growth factor is an implementation choice, so do not predict a universal capacity sequence. Always keep the returned value and reconsider aliases when storage may change.

Appending through one alias can overwrite elements visible through another alias when spare capacity is shared. A full slice expression s[low:high:max] can restrict capacity. That prevents this particular append from growing into the remaining shared region, but does not stop direct writes to the already shared elements.

Force append to allocate without guessing growth

The next program restricts the view’s length and capacity to two. Appending a third value must allocate separate backing storage. The later write into grown therefore cannot modify base. This is a deterministic conclusion from the capacity contract, not an observation of one runtime’s preferred growth strategy.

The capacity restriction is useful when returning a view that should not append into a caller’s trailing elements. It does not make the view immutable or establish exclusive ownership. If even existing-element mutations must be isolated, copy the elements instead. Choose the smallest promise that meets the API’s actual requirement.

go A capacity-limited view
package main

import "fmt"

func main() {
	base := []int{1, 2, 3}
	limited := base[:2:2]
	grown := append(limited, 7)
	grown[0] = 8
	fmt.Println(base, grown)
}
Expected output
[1 2 3] [8 2 7]

Appending beyond capacity creates independent storage; its exact capacity is intentionally not printed.

Range values are values, not element aliases

For a slice, range yields indices and copies of element values. Assigning to the value variable does not replace the slice element; use s[i] for that mutation. The range expression is evaluated before iteration according to the specification, and the iteration count does not grow merely because the loop appends to the slice variable. Avoid relying on subtle mutation effects.

For strings, len counts bytes, indexing yields a byte, and range decodes runes with byte positions. A byte slice can represent arbitrary data and is mutable. Converting between []byte and string has language-level value semantics; never infer permission for unsafe shared mutation from a compiler’s possible internal allocation optimizations.

Common errors and fixes

An element change unexpectedly affects another slice.

Why it happens
The slice descriptors share backing storage.
How to fix it
Copy when independent elements are required.
Prevent it next time
Document view versus copy semantics.

A valid capacity is used as an index bound.

Why it happens
Capacity was confused with current length.
How to fix it
Use i < len(s) before indexing.
Prevent it next time
Test empty and partially filled slices.

Changing a range value leaves the sequence unchanged.

Why it happens
The loop variable is an element-value copy.
How to fix it
Assign through the index or a deliberate pointer.
Prevent it next time
Review copy semantics for nested reference-bearing values.

Check your understanding

1. A slice has length two and capacity four. Can s[2] be read, and can s[:3] be formed? Explain both answers.

Show worked solution

s[2] is outside the current length and causes an index panic. Reslicing to s[:3] is allowed because the requested bound is within capacity, producing a new slice of length three. The newly exposed element already belongs to the backing array; its meaning depends on prior initialization and aliases. Capacity permits reslicing, not direct out-of-length indexing.

2. Design a function returning the first two int values without allowing mutations of the result to affect the input.

Show worked solution

Check that the input length is at least two, then allocate a two-element destination and copy the selected values. Return an error for shorter input under the chosen contract. Restricting capacity alone is insufficient because result[0] would still share the input element. For int elements, an element copy establishes the required independence without a recursive copy policy.

Key takeaways

  • A slice copy shares elements until an explicit copy or relevant reallocation separates them.
  • Length governs indexing; capacity governs permitted reslicing and append reuse.
  • Range and text processing require deliberate value and unit choices.

Official references