Skip to main content

Go Arrays and Slices

Arrays and slices are fundamental data structures in Go that provide efficient ways to store and manipulate sequential collections of data. While arrays have a fixed size, slices are dynamic and can grow or shrink as needed. Understanding the differences between arrays and slices, their operations, and performance characteristics is crucial for writing efficient Go programs. This comprehensive guide will teach you everything you need to know about Go's array and slice system.

Understanding Arrays and Slices in Go

What Are Arrays?

Arrays in Go are fixed-size sequences of elements of the same type. They provide:

  • Fixed size - Arrays have a predetermined length that cannot be changed
  • Type safety - All elements must be of the same type
  • Memory efficiency - Contiguous memory allocation for fast access
  • Index-based access - Elements can be accessed by their position (index)

What Are Slices?

Slices in Go are dynamic arrays that provide more flexibility than arrays. They offer:

  • Dynamic size - Slices can grow and shrink as needed
  • Flexible initialization - Can be created from arrays or other slices
  • Built-in operations - Rich set of operations for manipulation
  • Memory efficiency - Backed by arrays for optimal performance

Key Differences Between Arrays and Slices

FeatureArraysSlices
SizeFixed at compile timeDynamic at runtime
Declarationvar arr [5]intvar slice []int
Initializationarr := [5]int{1,2,3,4,5}slice := []int{1,2,3,4,5}
Lengthlen(arr)len(slice)
Capacitycap(arr) (same as length)cap(slice) (can be larger)
ResizingNot possiblePossible with append

Array Fundamentals

Array Declaration and Initialization

The var Keyword for Arrays

The var keyword is used to declare arrays with a specific type and size.

Array Literal Syntax

Arrays can be initialized using literal syntax with curly braces.

package main

import "fmt"

func main() {
// Array declaration and initialization examples
fmt.Println("Array declaration and initialization examples:")

// Declaration with var keyword
var arr1 [5]int
fmt.Printf("Array 1: %v\n", arr1)
// Output: Array 1: [0 0 0 0 0]

// Initialization with values
arr2 := [5]int{1, 2, 3, 4, 5}
fmt.Printf("Array 2: %v\n", arr2)
// Output: Array 2: [1 2 3 4 5]

// Partial initialization
arr3 := [5]int{1, 2, 3}
fmt.Printf("Array 3: %v\n", arr3)
// Output: Array 3: [1 2 3 0 0]

// Array literal with index specification
arr4 := [5]int{0: 10, 2: 30, 4: 50}
fmt.Printf("Array 4: %v\n", arr4)
// Output: Array 4: [10 0 30 0 50]

// Array with different types
arr5 := [3]string{"apple", "banana", "orange"}
fmt.Printf("Array 5: %v\n", arr5)
// Output: Array 5: [apple banana orange]

// Array with boolean values
arr6 := [4]bool{true, false, true, false}
fmt.Printf("Array 6: %v\n", arr6)
// Output: Array 6: [true false true false]
}

Array Operations and Access

The len Built-in Function

The len function returns the length (number of elements) of an array.

Array Indexing

Arrays use zero-based indexing to access individual elements.

package main

import "fmt"

func main() {
// Array operations and access examples
fmt.Println("Array operations and access examples:")

// Create an array
arr := [5]int{10, 20, 30, 40, 50}

// Get array length
length := len(arr)
fmt.Printf("Array length: %d\n", length)
// Output: Array length: 5

// Access individual elements
fmt.Printf("First element: %d\n", arr[0])
fmt.Printf("Second element: %d\n", arr[1])
fmt.Printf("Last element: %d\n", arr[length-1])
// Output:
// First element: 10
// Second element: 20
// Last element: 50

// Modify array elements
arr[0] = 100
arr[1] = 200
fmt.Printf("Modified array: %v\n", arr)
// Output: Modified array: [100 200 30 40 50]

// Iterate over array
fmt.Println("Array elements:")
for i := 0; i < len(arr); i++ {
fmt.Printf(" arr[%d] = %d\n", i, arr[i])
}
// Output:
// Array elements:
// arr[0] = 100
// arr[1] = 200
// arr[2] = 30
// arr[3] = 40
// arr[4] = 50

// Iterate with range
fmt.Println("Array elements with range:")
for index, value := range arr {
fmt.Printf(" arr[%d] = %d\n", index, value)
}
// Output:
// Array elements with range:
// arr[0] = 100
// arr[1] = 200
// arr[2] = 30
// arr[3] = 40
// arr[4] = 50
}

Array Characteristics and Limitations

Arrays in Go have specific characteristics that affect their usage:

package main

import "fmt"

func main() {
// Array characteristics and limitations examples
fmt.Println("Array characteristics and limitations examples:")

// Array size is part of the type
var arr1 [3]int
var arr2 [5]int
fmt.Printf("Array 1 type: %T\n", arr1)
fmt.Printf("Array 2 type: %T\n", arr2)
// Output:
// Array 1 type: [3]int
// Array 2 type: [5]int

// Arrays of different sizes are different types
// arr1 = arr2 // This would cause a compilation error

// Array comparison (only if same type)
arr3 := [3]int{1, 2, 3}
arr4 := [3]int{1, 2, 3}
arr5 := [3]int{1, 2, 4}

fmt.Printf("arr3 == arr4: %t\n", arr3 == arr4)
fmt.Printf("arr3 == arr5: %t\n", arr3 == arr5)
// Output:
// arr3 == arr4: true
// arr3 == arr5: false

// Array copying
arr6 := [4]int{1, 2, 3, 4}
arr7 := arr6 // Copy the array
arr7[0] = 100
fmt.Printf("Original array: %v\n", arr6)
fmt.Printf("Copied array: %v\n", arr7)
// Output:
// Original array: [1 2 3 4]
// Copied array: [100 2 3 4]

// Array as function parameter
result := sumArray([5]int{1, 2, 3, 4, 5})
fmt.Printf("Sum of array: %d\n", result)
// Output: Sum of array: 15
}

// Function that takes an array as parameter
func sumArray(arr [5]int) int {
sum := 0
for _, value := range arr {
sum += value
}
return sum
}

Slice Fundamentals

Slice Declaration and Creation

The make Built-in Function

The make function is used to create slices with specific length and capacity.

Slice Literal Syntax

Slices can be created using literal syntax similar to arrays.

package main

import "fmt"

func main() {
// Slice declaration and creation examples
fmt.Println("Slice declaration and creation examples:")

// Declaration with var keyword
var slice1 []int
fmt.Printf("Slice 1: %v (length: %d, capacity: %d)\n", slice1, len(slice1), cap(slice1))
// Output: Slice 1: [] (length: 0, capacity: 0)

// Creation with make function
slice2 := make([]int, 5) // length 5, capacity 5
fmt.Printf("Slice 2: %v (length: %d, capacity: %d)\n", slice2, len(slice2), cap(slice2))
// Output: Slice 2: [0 0 0 0 0] (length: 5, capacity: 5)

// Creation with make function specifying capacity
slice3 := make([]int, 3, 10) // length 3, capacity 10
fmt.Printf("Slice 3: %v (length: %d, capacity: %d)\n", slice3, len(slice3), cap(slice3))
// Output: Slice 3: [0 0 0] (length: 3, capacity: 10)

// Slice literal
slice4 := []int{1, 2, 3, 4, 5}
fmt.Printf("Slice 4: %v (length: %d, capacity: %d)\n", slice4, len(slice4), cap(slice4))
// Output: Slice 4: [1 2 3 4 5] (length: 5, capacity: 5)

// Slice from array
arr := [5]int{10, 20, 30, 40, 50}
slice5 := arr[:] // slice of entire array
fmt.Printf("Slice 5: %v (length: %d, capacity: %d)\n", slice5, len(slice5), cap(slice5))
// Output: Slice 5: [10 20 30 40 50] (length: 5, capacity: 5)

// Slice from array (partial)
slice6 := arr[1:4] // slice from index 1 to 3
fmt.Printf("Slice 6: %v (length: %d, capacity: %d)\n", slice6, len(slice6), cap(slice6))
// Output: Slice 6: [20 30 40] (length: 3, capacity: 4)

// Slice from slice
slice7 := slice4[1:3] // slice from index 1 to 2
fmt.Printf("Slice 7: %v (length: %d, capacity: %d)\n", slice7, len(slice7), cap(slice7))
// Output: Slice 7: [2 3] (length: 2, capacity: 4)
}

Slice Operations and Built-in Functions

The append Built-in Function

The append function adds elements to a slice and returns a new slice.

The copy Built-in Function

The copy function copies elements from one slice to another.

The len and cap Built-in Functions

The len function returns the length, and cap returns the capacity of a slice.

package main

import "fmt"

func main() {
// Slice operations and built-in functions examples
fmt.Println("Slice operations and built-in functions examples:")

// Create initial slice
slice := []int{1, 2, 3}
fmt.Printf("Initial slice: %v (length: %d, capacity: %d)\n", slice, len(slice), cap(slice))
// Output: Initial slice: [1 2 3] (length: 3, capacity: 3)

// Append single element
slice = append(slice, 4)
fmt.Printf("After append(4): %v (length: %d, capacity: %d)\n", slice, len(slice), cap(slice))
// Output: After append(4): [1 2 3 4] (length: 4, capacity: 6)

// Append multiple elements
slice = append(slice, 5, 6, 7)
fmt.Printf("After append(5,6,7): %v (length: %d, capacity: %d)\n", slice, len(slice), cap(slice))
// Output: After append(5,6,7): [1 2 3 4 5 6 7] (length: 7, capacity: 12)

// Append slice to slice
slice2 := []int{8, 9, 10}
slice = append(slice, slice2...)
fmt.Printf("After append slice: %v (length: %d, capacity: %d)\n", slice, len(slice), cap(slice))
// Output: After append slice: [1 2 3 4 5 6 7 8 9 10] (length: 10, capacity: 12)

// Copy slice
slice3 := make([]int, len(slice))
copied := copy(slice3, slice)
fmt.Printf("Copied slice: %v (elements copied: %d)\n", slice3, copied)
// Output: Copied slice: [1 2 3 4 5 6 7 8 9 10] (elements copied: 10)

// Modify original slice
slice[0] = 100
fmt.Printf("Original slice: %v\n", slice)
fmt.Printf("Copied slice: %v\n", slice3)
// Output:
// Original slice: [100 2 3 4 5 6 7 8 9 10]
// Copied slice: [1 2 3 4 5 6 7 8 9 10]

// Slice operations
fmt.Printf("Slice length: %d\n", len(slice))
fmt.Printf("Slice capacity: %d\n", cap(slice))
// Output:
// Slice length: 10
// Slice capacity: 12
}

Slice Slicing and Indexing

Slice Expression Syntax

Slices can be created using slice expressions with start and end indices.

Slice Bounds

Slice bounds determine which elements are included in the slice.

package main

import "fmt"

func main() {
// Slice slicing and indexing examples
fmt.Println("Slice slicing and indexing examples:")

// Create base slice
base := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
fmt.Printf("Base slice: %v\n", base)
// Output: Base slice: [0 1 2 3 4 5 6 7 8 9]

// Slice from start to end
slice1 := base[2:8]
fmt.Printf("base[2:8]: %v (length: %d, capacity: %d)\n", slice1, len(slice1), cap(slice1))
// Output: base[2:8]: [2 3 4 5 6 7] (length: 6, capacity: 8)

// Slice from start to end (omitting end)
slice2 := base[2:]
fmt.Printf("base[2:]: %v (length: %d, capacity: %d)\n", slice2, len(slice2), cap(slice2))
// Output: base[2:]: [2 3 4 5 6 7 8 9] (length: 8, capacity: 8)

// Slice from start to end (omitting start)
slice3 := base[:6]
fmt.Printf("base[:6]: %v (length: %d, capacity: %d)\n", slice3, len(slice3), cap(slice3))
// Output: base[:6]: [0 1 2 3 4 5] (length: 6, capacity: 10)

// Slice entire slice
slice4 := base[:]
fmt.Printf("base[:]: %v (length: %d, capacity: %d)\n", slice4, len(slice4), cap(slice4))
// Output: base[:]: [0 1 2 3 4 5 6 7 8 9] (length: 10, capacity: 10)

// Slice with capacity specification
slice5 := base[2:8:8] // start:2, end:8, capacity:8
fmt.Printf("base[2:8:8]: %v (length: %d, capacity: %d)\n", slice5, len(slice5), cap(slice5))
// Output: base[2:8:8]: [2 3 4 5 6 7] (length: 6, capacity: 6)

// Demonstrate shared underlying array
slice1[0] = 100
fmt.Printf("Modified slice1: %v\n", slice1)
fmt.Printf("Base slice: %v\n", base)
// Output:
// Modified slice1: [100 3 4 5 6 7]
// Base slice: [0 1 100 3 4 5 6 7 8 9]
}

Advanced Slice Patterns

Slice as Function Parameters

Slices are passed by reference to functions, making them efficient for large data.

package main

import "fmt"

func main() {
// Slice as function parameters examples
fmt.Println("Slice as function parameters examples:")

// Create slice
numbers := []int{1, 2, 3, 4, 5}
fmt.Printf("Original slice: %v\n", numbers)
// Output: Original slice: [1 2 3 4 5]

// Pass slice to function
doubleSlice(numbers)
fmt.Printf("After doubling: %v\n", numbers)
// Output: After doubling: [2 4 6 8 10]

// Pass slice to function that returns new slice
doubled := doubleSliceReturn(numbers)
fmt.Printf("Original: %v\n", numbers)
fmt.Printf("Doubled: %v\n", doubled)
// Output:
// Original: [2 4 6 8 10]
// Doubled: [4 8 12 16 20]

// Pass slice to function that appends elements
extended := extendSlice(numbers, 11, 12, 13)
fmt.Printf("Extended slice: %v\n", extended)
// Output: Extended slice: [2 4 6 8 10 11 12 13]
}

// Function that modifies slice in place
func doubleSlice(slice []int) {
for i := range slice {
slice[i] *= 2
}
}

// Function that returns new slice
func doubleSliceReturn(slice []int) []int {
result := make([]int, len(slice))
for i, value := range slice {
result[i] = value * 2
}
return result
}

// Function that extends slice
func extendSlice(slice []int, values ...int) []int {
return append(slice, values...)
}

Slice Memory Management

Understanding slice memory management is crucial for performance optimization.

package main

import "fmt"

func main() {
// Slice memory management examples
fmt.Println("Slice memory management examples:")

// Create slice with initial capacity
slice := make([]int, 0, 10) // length 0, capacity 10
fmt.Printf("Initial: length=%d, capacity=%d\n", len(slice), cap(slice))
// Output: Initial: length=0, capacity=10

// Add elements without reallocation
for i := 0; i < 5; i++ {
slice = append(slice, i)
fmt.Printf("After append %d: length=%d, capacity=%d\n", i, len(slice), cap(slice))
}
// Output:
// After append 0: length=1, capacity=10
// After append 1: length=2, capacity=10
// After append 2: length=3, capacity=10
// After append 3: length=4, capacity=10
// After append 4: length=5, capacity=10

// Continue adding elements (will trigger reallocation)
for i := 5; i < 15; i++ {
slice = append(slice, i)
if len(slice) == cap(slice) {
fmt.Printf("Reallocation at length %d, new capacity: %d\n", len(slice), cap(slice))
}
}
// Output:
// Reallocation at length 10, new capacity: 20
// Reallocation at length 20, new capacity: 40

// Slice reuse pattern
slice = slice[:0] // Reset length to 0, keep capacity
fmt.Printf("After reset: length=%d, capacity=%d\n", len(slice), cap(slice))
// Output: After reset: length=0, capacity=40

// Reuse the slice
for i := 0; i < 5; i++ {
slice = append(slice, i*10)
}
fmt.Printf("Reused slice: %v (length=%d, capacity=%d)\n", slice, len(slice), cap(slice))
// Output: Reused slice: [0 10 20 30 40] (length=5, capacity=40)
}

Slice Patterns and Best Practices

Following slice patterns and best practices ensures efficient and maintainable code.

package main

import "fmt"

func main() {
// Slice patterns and best practices examples
fmt.Println("Slice patterns and best practices examples:")

// Pattern 1: Filter slice
numbers := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
evens := filterEven(numbers)
fmt.Printf("Original: %v\n", numbers)
fmt.Printf("Even numbers: %v\n", evens)
// Output:
// Original: [1 2 3 4 5 6 7 8 9 10]
// Even numbers: [2 4 6 8 10]

// Pattern 2: Map slice
doubled := mapSlice(numbers, func(x int) int { return x * 2 })
fmt.Printf("Doubled: %v\n", doubled)
// Output: Doubled: [2 4 6 8 10 12 14 16 18 20]

// Pattern 3: Reduce slice
sum := reduceSlice(numbers, 0, func(acc, x int) int { return acc + x })
fmt.Printf("Sum: %d\n", sum)
// Output: Sum: 55

// Pattern 4: Slice chunking
chunks := chunkSlice(numbers, 3)
fmt.Printf("Chunks of 3: %v\n", chunks)
// Output: Chunks of 3: [[1 2 3] [4 5 6] [7 8 9] [10]]

// Pattern 5: Slice deduplication
duplicates := []int{1, 2, 2, 3, 3, 3, 4, 4, 4, 4}
unique := deduplicateSlice(duplicates)
fmt.Printf("Original: %v\n", duplicates)
fmt.Printf("Unique: %v\n", unique)
// Output:
// Original: [1 2 2 3 3 3 4 4 4 4]
// Unique: [1 2 3 4]
}

// Filter slice based on condition
func filterEven(numbers []int) []int {
var result []int
for _, num := range numbers {
if num%2 == 0 {
result = append(result, num)
}
}
return result
}

// Map slice using function
func mapSlice(numbers []int, fn func(int) int) []int {
result := make([]int, len(numbers))
for i, num := range numbers {
result[i] = fn(num)
}
return result
}

// Reduce slice using function
func reduceSlice(numbers []int, initial int, fn func(int, int) int) int {
result := initial
for _, num := range numbers {
result = fn(result, num)
}
return result
}

// Chunk slice into smaller slices
func chunkSlice(slice []int, chunkSize int) [][]int {
var chunks [][]int
for i := 0; i < len(slice); i += chunkSize {
end := i + chunkSize
if end > len(slice) {
end = len(slice)
}
chunks = append(chunks, slice[i:end])
}
return chunks
}

// Remove duplicates from slice
func deduplicateSlice(slice []int) []int {
seen := make(map[int]bool)
var result []int
for _, num := range slice {
if !seen[num] {
seen[num] = true
result = append(result, num)
}
}
return result
}

What You've Learned

Congratulations! You now have a comprehensive understanding of Go's arrays and slices:

Array Fundamentals

  • Understanding array declaration and initialization
  • Working with array operations and indexing
  • Understanding array characteristics and limitations
  • Using arrays as function parameters

Slice Fundamentals

  • Creating slices with make and literal syntax
  • Understanding slice operations and built-in functions
  • Working with slice slicing and indexing
  • Understanding slice memory management

Advanced Slice Patterns

  • Using slices as function parameters
  • Implementing slice patterns and best practices
  • Understanding memory management and performance
  • Creating reusable slice operations

Built-in Functions

  • make - Creating slices with specific length and capacity
  • append - Adding elements to slices
  • copy - Copying elements between slices
  • len - Getting slice length
  • cap - Getting slice capacity

Next Steps

You now have a solid foundation in Go's arrays and slices. In the next section, we'll explore Go's map data structure, which provides efficient key-value storage and retrieval.

Understanding arrays and slices is crucial for working with sequential data collections. These concepts form the foundation for all the more advanced programming techniques we'll cover in the coming chapters.


Ready to learn about maps? Let's explore Go's powerful map data structure and learn how to work with key-value pairs!