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

GO
// 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

GO
// 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
💡 Tip: 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

GO
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
💡 Tip: 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.

💡 Tip: When using 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 ⭐)

GO
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))
}
▶ Try it Yourself

Output:

TEXT
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 ⭐⭐)

GO
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)
}
▶ Try it Yourself

Output:

TEXT
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 ⭐⭐⭐)

GO
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()
}
▶ Try it Yourself

Output:

TEXT
=== 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)

GO
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

GO
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.

GO
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:

GO
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


📝 Exercises

Exercise 1 (⭐)

Write a program that creates a slice of 10 integers (values 1~10), then:

  1. Print the slice's length and capacity
  2. Slice from index 2 to 7, print its contents, length, and capacity
  3. 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:


Next Lesson

Next Lesson: Map →

Web-Tutorial.com

Web-Tutorial Tech Team

A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏