Arrays and Slices
Arrays and Slices
Imagine you buy a whole crate of milk at the supermarket — an array is like that fixed-capacity crate that can't hold any more once full; a slice is like a shopping bag that can keep expanding — when you have more items, just switch to a bigger bag. In Go, arrays have a fixed length and are value types; slices have a variable length and are reference types. In practice, slices are used far more often than arrays, but understanding arrays is the foundation for understanding slices.
1. Core Concepts
| Concept | Description |
|---|---|
Array [N]T |
Fixed length; size must be specified at declaration; value type — assignment and parameter passing copy the entire array |
Slice []T |
Variable length; references an underlying array; reference type — assignment and parameter passing share the underlying array |
make([]T, len, cap) |
Recommended way to create slices; can specify length and capacity |
append(slice, elems...) |
Appends elements to a slice, returns a new slice; may trigger underlying array expansion |
copy(dst, src) |
Copies contents from src slice to dst, returns the number of elements copied |
Length len() |
The actual number of elements in the slice |
Capacity cap() |
The number of elements from the slice's starting position to the end of the underlying array |
2. Basic Syntax/Usage
Array Declaration and Initialization
// Declare an int array of length 5, zero-value initialized
var arr1 [5]int
// Declare and initialize
var arr2 = [5]int{1, 2, 3, 4, 5}
// Let the compiler infer the length
arr3 := [...]int{10, 20, 30}
// Initialize with specific indices
arr4 := [5]int{1: 100, 3: 300} // [0, 100, 0, 300, 0]
Slice Declaration and Initialization
// Declare a nil slice (zero value is nil, but can directly append)
var s1 []int
// Literal initialization
s2 := []int{1, 2, 3}
// Create slice from array
arr := [5]int{10, 20, 30, 40, 50}
s3 := arr[1:4] // [20, 30, 40]
// Create slice with make
s4 := make([]int, 5) // Length 5, capacity 5
s5 := make([]int, 3, 10) // Length 3, capacity 10
nil slices and empty slices ([]int{} or make([]int, 0)) are functionally equivalent — len and cap are both 0, and append works normally. The difference is that nil slices serialize to null in JSON, while empty slices serialize to [].
Slice Operations
s := []int{10, 20, 30, 40, 50}
// Slice a sub-slice [left-closed, right-open)
s1 := s[1:3] // [20, 30]
s2 := s[:3] // [10, 20, 30], from the beginning
s3 := s[2:] // [30, 40, 50], to the end
s4 := s[:] // [10, 20, 30, 40, 50], full slice
// Length and capacity
fmt.Println(len(s)) // 5
fmt.Println(cap(s)) // 5
// append elements
s = append(s, 60) // Append single element
s = append(s, 70, 80, 90) // Append multiple elements
s = append(s, []int{100}...) // Append another slice
// copy slices
src := []int{1, 2, 3}
dst := make([]int, len(src))
n := copy(dst, src) // dst = [1, 2, 3], n = 3
append returns a new slice; you must capture the return value. When len(s) == cap(s), append allocates a new underlying array, and the old and new slices no longer share data.
s[low:high] to slice, the new slice shares the underlying array with the original. Modifying elements in the sub-slice affects the original. Use copy if you need an independent copy.
3. Example Code
Example 1: Basic Usage (Difficulty ⭐)
package main
import "fmt"
func main() {
// ========== Arrays ==========
// Declare and initialize an array of 5 scores
scores := [5]int{90, 85, 78, 92, 88}
fmt.Println("Score array:", scores)
// Access and modify elements by index
fmt.Println("First score:", scores[0])
scores[2] = 80 // Change the third score to 80
fmt.Println("After modification:", scores)
// Array traversal: traditional for loop
fmt.Println("\n--- Traditional for traversal ---")
for i := 0; i < len(scores); i++ {
fmt.Printf("Score %d: %d\n", i+1, scores[i])
}
// Array traversal: range
fmt.Println("\n--- Range traversal ---")
for index, value := range scores {
fmt.Printf("Index %d: %d\n", index, value)
}
// ========== Slices ==========
// Derive slice from array
top3 := scores[0:3] // First 3 scores
fmt.Println("\nFirst 3 scores:", top3)
// Create slice with literal
fruits := []string{"Apple", "Banana", "Orange"}
fmt.Println("Fruit slice:", fruits)
// Add element with append
fruits = append(fruits, "Grape")
fmt.Println("After adding Grape:", fruits)
// Length and capacity
fmt.Printf("Length: %d, Capacity: %d\n", len(fruits), cap(fruits))
}
Output:
Score array: [90 85 78 92 88]
First score: 90
After modification: [90 85 80 92 88]
--- Traditional for traversal ---
Score 1: 90
Score 2: 85
Score 3: 80
Score 4: 92
Score 5: 88
--- Range traversal ---
Index 0: 90
Index 1: 85
Index 2: 80
Index 3: 92
Index 4: 88
First 3 scores: [90 85 80]
Fruit slice: [Apple Banana Orange]
After adding Grape: [Apple Banana Orange Grape]
Length: 4, Capacity: 4
Example 2: Intermediate Usage (Difficulty ⭐⭐)
package main
import "fmt"
func main() {
// ========== append and expansion ==========
// Create a slice with capacity 3
s := make([]int, 0, 3)
fmt.Printf("Initial: len=%d, cap=%d, %v\n", len(s), cap(s), s)
// Append one by one, observe capacity changes
for i := 1; i <= 5; i++ {
s = append(s, i)
fmt.Printf("After adding %d: len=%d, cap=%d, %v\n", i, len(s), cap(s), s)
}
// ========== Slices sharing underlying array ==========
original := []int{10, 20, 30, 40, 50}
sub := original[1:3] // [20, 30]
fmt.Println("\n--- Shared underlying array demo ---")
fmt.Println("Original slice:", original)
fmt.Println("Sub-slice: ", sub)
// Modifying the sub-slice affects the original slice
sub[0] = 999
fmt.Println("\nAfter modifying sub-slice:")
fmt.Println("Original slice:", original) // original[1] also changed
fmt.Println("Sub-slice: ", sub)
// ========== Using copy to create independent copy ==========
fmt.Println("\n--- Using copy for independent copy ---")
original = []int{10, 20, 30, 40, 50}
// Slice first, then copy independently
subSlice := original[1:4] // [20, 30, 40]
independent := make([]int, len(subSlice))
copy(independent, subSlice)
independent[0] = 888
fmt.Println("Original slice:", original) // Unaffected
fmt.Println("Independent copy:", independent) // Only copy changed
// ========== Merging two slices ==========
fmt.Println("\n--- Merging slices ---")
a := []int{1, 2, 3}
b := []int{4, 5, 6}
merged := append(a, b...)
fmt.Println("Merge result:", merged)
}
Output:
Initial: len=0, cap=3, []
After adding 1: len=1, cap=3, [1]
After adding 2: len=2, cap=3, [1 2]
After adding 3: len=3, cap=3, [1 2 3]
After adding 4: len=4, cap=6, [1 2 3 4]
After adding 5: len=5, cap=6, [1 2 3 4 5]
--- Shared underlying array demo ---
Original slice: [10 20 30 40 50]
Sub-slice: [20 30]
After modifying sub-slice:
Original slice: [10 999 30 40 50]
Sub-slice: [999 30]
--- Using copy for independent copy ---
Original slice: [10 20 30 40 50]
Independent copy: [888 30 40]
--- Merging slices ---
Merge result: [1 2 3 4 5 6]
Example 3: Comprehensive Application (Difficulty ⭐⭐⭐)
package main
import "fmt"
// removeElement removes the element at the specified index from a slice (maintains order)
func removeElement(s []int, index int) []int {
if index < 0 || index >= len(s) {
return s // Index out of bounds, return original slice
}
// Use append to concatenate parts before and after index
return append(s[:index], s[index+1:]...)
}
// insertElement inserts an element at the specified index
func insertElement(s []int, index int, value int) []int {
if index < 0 || index > len(s) {
return s
}
// First expand the slice, then shift elements, then assign
s = append(s, 0) // Append a placeholder element
copy(s[index+1:], s[index:]) // Shift elements from index onward by one
s[index] = value // Assign value at target position
return s
}
// filterEven filters out even numbers, returns a new slice
func filterEven(s []int) []int {
result := make([]int, 0, len(s)/2) // Estimate capacity as half
for _, v := range s {
if v%2 == 0 {
result = append(result, v)
}
}
return result
}
// Test slice memory sharing and expansion mechanism
func sliceInternals() {
fmt.Println("=== Slice Internals Demo ===")
// Create an underlying array
data := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
// Slice a sub-slice (shares underlying array)
s1 := data[2:5] // [2, 3, 4], len=3, cap=8
s2 := s1[1:3] // [3, 4], len=2, cap=7
fmt.Printf("data: %v, len=%d, cap=%d\n", data, len(data), cap(data))
fmt.Printf("s1: %v, len=%d, cap=%d\n", s1, len(s1), cap(s1))
fmt.Printf("s2: %v, len=%d, cap=%d\n", s2, len(s2), cap(s2))
// s2 modifying elements affects s1 and data
s2[0] = 999
fmt.Printf("\nAfter modifying s2[0]=999:\n")
fmt.Printf("data: %v\n", data) // data[4] changed to 999
fmt.Printf("s1: %v\n", s1) // s1[2] changed to 999
fmt.Printf("s2: %v\n", s2) // s2[0] changed to 999
// append may cause disconnection
fmt.Println("\n--- append causes expansion ---")
s3 := data[0:2] // [0, 1], len=2, cap=10
fmt.Printf("Before append s3: %v, len=%d, cap=%d\n", s3, len(s3), cap(s3))
// append within capacity, still shared
s3 = append(s3, 99)
fmt.Printf("After append(99) s3: %v, len=%d, cap=%d\n", s3, len(s3), cap(s3))
fmt.Printf("data[2] = %d (modified!)\n", data[2])
// After exceeding capacity, new array is allocated
s3 = append(s3, 100, 200, 300, 400, 500, 600, 700, 800)
fmt.Printf("After large append s3: %v, len=%d, cap=%d\n", s3, len(s3), cap(s3))
fmt.Printf("data unaffected: %v\n", data)
}
func main() {
// ========== Slice Operations in Practice ==========
fmt.Println("=== Slice Operations in Practice ===")
// Initial data
nums := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
fmt.Println("Original data:", nums)
// Remove element at index 4 (value 5)
nums = removeElement(nums, 4)
fmt.Println("After removing index 4:", nums)
// Insert 100 at index 2
nums = insertElement(nums, 2, 100)
fmt.Println("After inserting 100 at index 2:", nums)
// Filter even numbers
evens := filterEven(nums)
fmt.Println("Even slice:", evens)
// ========== Slice as Stack ==========
fmt.Println("\n=== Slice Simulating Stack ===")
var stack []int
// Push
for i := 1; i <= 5; i++ {
stack = append(stack, i)
fmt.Printf("Push %d -> Stack: %v\n", i, stack)
}
// Pop (from the end)
for len(stack) > 0 {
// Get last element
top := stack[len(stack)-1]
stack = stack[:len(stack)-1]
fmt.Printf("Pop %d -> Stack: %v\n", top, stack)
}
// ========== Internals Demo ==========
fmt.Println()
sliceInternals()
}
Output:
=== Slice Operations in Practice ===
Original data: [1 2 3 4 5 6 7 8 9 10]
After removing index 4: [1 2 3 4 6 7 8 9 10]
After inserting 100 at index 2: [1 2 100 3 4 6 7 8 9 10]
Even slice: [2 100 4 6 8 10]
=== Slice Simulating Stack ===
Push 1 -> Stack: [1]
Push 2 -> Stack: [1 2]
Push 3 -> Stack: [1 2 3]
Push 4 -> Stack: [1 2 3 4]
Push 5 -> Stack: [1 2 3 4 5]
Pop 5 -> Stack: [1 2 3 4]
Pop 4 -> Stack: [1 2 3]
Pop 3 -> Stack: [1 2]
Pop 2 -> Stack: [1]
Pop 1 -> Stack: []
=== Slice Internals Demo ===
data: [0 1 2 3 4 5 6 7 8 9], len=10, cap=10
s1: [2 3 4], len=3, cap=8
s2: [3 4], len=2, cap=7
After modifying s2[0]=999:
data: [0 1 2 3 999 5 6 7 8 9]
s1: [2 3 999]
s2: [3 999]
--- append causes expansion ---
Before append s3: [0 1], len=2, cap=10
After append(99) s3: [0 1 99], len=3, cap=10
data[2] = 99 (modified!)
After large append s3: [0 1 99 100 200 300 400 500 600 700 800], len=12, cap=20
data unaffected: [0 1 99 3 999 5 6 7 8 9]
3. Common Use Cases
Case 1: Batch Data Processing (Filter and Transform)
package main
import "fmt"
// cleanData filters out empty strings from a string slice and converts to uppercase
func cleanData(input []string) []string {
result := make([]string, 0, len(input))
for _, s := range input {
if s != "" {
// Convert to uppercase (simplified example, use strings.ToUpper in practice)
upper := ""
for _, c := range s {
if c >= 'a' && c <= 'z' {
upper += string(c - 32)
} else {
upper += string(c)
}
}
result = append(result, upper)
}
}
return result
}
func main() {
raw := []string{"hello", "", "world", "", "go", "lang"}
cleaned := cleanData(raw)
fmt.Println("Cleaned:", cleaned) // [HELLO WORLD GO LANG]
}
Case 2: Implementing a Dynamic Queue
package main
import "fmt"
// Queue implements a simple FIFO queue using a slice
type Queue struct {
items []string
}
// Enqueue adds an item to the queue
func (q *Queue) Enqueue(item string) {
q.items = append(q.items, item)
}
// Dequeue removes and returns the first item
func (q *Queue) Dequeue() (string, bool) {
if len(q.items) == 0 {
return "", false
}
item := q.items[0]
q.items = q.items[1:] // Remove first element
return item, true
}
// Size returns the queue size
func (q *Queue) Size() int {
return len(q.items)
}
func main() {
q := &Queue{}
q.Enqueue("Task A")
q.Enqueue("Task B")
q.Enqueue("Task C")
fmt.Printf("Queue size: %d\n", q.Size())
for q.Size() > 0 {
item, _ := q.Dequeue()
fmt.Println("Processing:", item)
}
}
❓ FAQ
Q1: Is the slice declared with var s []int a nil slice? Can it be used directly?
Yes. A nil slice has len and cap of 0, and append works perfectly. Go's official recommendation: if a slice might be nil, no extra nil check is needed before use — just append directly.
var s []int // nil slice
s = append(s, 1, 2) // Completely valid
fmt.Println(s) // [1 2]
Q2: Does slicing with s[2:5] affect the original slice?
Yes. The sub-slice produced by slicing shares the underlying array with the original. Modifying elements in the sub-slice will be reflected in the original. Use copy for an independent copy:
original := []int{1, 2, 3, 4, 5}
sub := make([]int, 3)
copy(sub, original[2:5]) // Independent copy, no mutual effect
Q3: Why did the value change after append?
When len(s) < cap(s), append writes directly to the underlying array without creating a new one. If other slices reference later positions in the same underlying array, they'll see "unexpected" changes. Solution: use copy to create an independent copy before append.
Q4: How does slice capacity grow?
The Go runtime determines new capacity based on the current slice capacity. The typical strategy is: double the capacity when less than 1024, grow by about 1.25x when greater than 1024. The specific strategy may change across Go versions, so don't rely on exact growth rules.
📖 Summary
- Array
[N]Thas a fixed length, is a value type; assignment/parameter passing copies the entire array - Slice
[]Thas a variable length, is a reference type; references an underlying array make([]T, len, cap)is the recommended way to create slices; pre-allocating capacity avoids frequent expansionappendreturns a new slice; you must capture the return value; allocates a new underlying array when capacity is insufficient- Slice slicing
s[low:high]shares the underlying array with the original; changes affect each other copy(dst, src)creates an independent copy of a slicelen()returns the number of elements;cap()returns the capacity of the underlying array- The zero value of a slice is
nil; you canappenddirectly
📝 Exercises
Exercise 1 (⭐)
Write a program that creates a slice of 10 integers (values 1~10), then:
- Print the slice's length and capacity
- Slice from index 2 to 7, print its contents, length, and capacity
- Use range to iterate over the sub-slice, printing each element and its index
Exercise 2 (⭐⭐)
Write a unique function that accepts an []int slice and returns a new slice with duplicates removed (maintaining the order of first occurrence). For example, input [1, 3, 2, 3, 1, 4, 2] should return [1, 3, 2, 4].
Hint: Use an auxiliary slice to record elements that have already appeared.
Exercise 3 (⭐⭐⭐)
Write a mergeSorted function that accepts two sorted []int slices and merges them into one sorted slice. Requirements:
- Time complexity must be O(n); do not merge then sort
- For example, input
[1, 3, 5, 7]and[2, 4, 6, 8]should return[1, 2, 3, 4, 5, 6, 7, 8] - Think: What role does this operation play in merge sort?



