Go: Go Arrays and Slices
Slices are at the heart of how Go handles collection data—they are lightweight "views" of arrays, and their ability to resize dynamically makes collection operations both fast and flexible.
Arrays are rarely used directly in Go (because their length is fixed), but slices are everywhere. To understand slices, you must first understand arrays—at their core, a slice is a pointer to an مصفوفة plus its length and capacity. In this lesson, you'll master all the core concepts of Go's collection types.
1. You will learn
- Defining, initializing, iterating through, and comparing arrays
- 4 Ways to Create Slices (Literal/مصفوفة/make/فارغ value)
- The difference between
len()andcap() - The meanings of the three arguments for
make appendautomatic resizing mechanism- Slice
[low:high:max] - Pitfalls of Sharing the Underlying Array in Slicing (Key Point)
- Implementing a dynamic data collector using slices
2. A True Story of a Data Acquisition Engineer
(1) Pain Point: A fixed-size مصفوفة cannot accommodate dynamic data
Alice is a data acquisition engineer. She recently needed to write a web crawler:
"I need to retrieve 10,000 records from each of 5 data sources and store them in memory to remove duplicates. At first, I used Python lists—just a simple 'append' and I was done. After switching to Go, I used a fixed-length مصفوفة like
[10000]int, but it panicked on the very first record: 'index out of range'."
She opened the Go code she had written:
// Version 1: Fixed-length array; cannot grow
var records [10000]int
for i := 0; i < 50000; i++ {
records[i] = i // ❌ i >= 10000 causes panic!
}
The boss said, "The number of data sources changes every day, so your fixed-length array won't work." Only then did Alice realize that Go arrays are value types, and the length is part of the type—[5]int and [10]int are different types.
(2) Solution in Go: Using slices
// data_collector.go
package main
import "fmt"
func main() {
// Slices: Dynamic length, automatic resizing
var records []int // nil slice, can append
// Simulate retrieving 10,000 records from each of 5 data sources
for source := 1; source <= 5; source++ {
for i := 0; i < 10000; i++ {
value := source*10000 + i
records = append(records, value) // auto-resize
}
}
fmt.Printf("Total number of records: %d\n", len(records))
fmt.Printf("Underlying capacity: %d\n", cap(records))
fmt.Printf("Top 3: %v\n", records[:3])
fmt.Printf("Last 3: %v\n", records[len(records)-3:])
}
Output:
Total number of records: 50000
Underlying capacity: 65536
Top 3: [10000 10001 10002]
Last 3: [59997 59998 59999]
(3) Performance: مصفوفة vs. slice
| Dimension | مصفوفة [n]T |
slice []T |
|---|---|---|
| Length | Fixed at compile time | Variable at runtime |
| Type | [5]int ≠ [10]int |
[]int Generic |
| Parameter Passing | Value Passing (copies the entire مصفوفة) | Reference Passing (only 24 bytes) |
| Resize | Not supported | append automatic |
| Use Case | Fixed Size (e.g., مخزن مؤقت) | 99% of Cases |
3. مصفوفة
(1) Defining and Initializing Arrays
package main
import "fmt"
func main() {
// Method 1: Declaration + Initialization
var a1 [5]int = [5]int{1, 2, 3, 4, 5}
// Method 2: Type Inference
a2 := [5]int{1, 2, 3, 4, 5}
// Method 3: Partial Initialization (with the Rest Set to Zero)
a3 := [5]int{1, 2} // [1 2 0 0 0]
// Method 4: Use ... to let the compiler infer the length
a4 := [...]int{1, 2, 3, 4} // length 4
// Method 5: Specifying Index Initialization
a5 := [5]int{0: 10, 4: 50} // [10 0 0 0 50]
fmt.Println(a1, a2, a3, a4, a5)
}
Output:
[1 2 3 4 5] [1 2 3 4 5] [1 2 0 0 0] [1 2 3 4] [10 0 0 0 50]
(2) Iterating Through Arrays
package main
import "fmt"
func main() {
nums := [4]سلسلة{"Alice", "Bob", "Charlie", "Dave"}
// Method 1: for حلقة
for i := 0; i < len(nums); i++ {
fmt.Printf("%d: %s\n", i, nums[i])
}
// Method 2: for range
for i, name := range nums {
fmt.Printf("[%d] %s\n", i, name)
}
}
(3) Arrays are value types
package main
import "fmt"
func modify(arr [3]int) {
arr[0] = 999 // modifies the copy
fmt.Printf("Inside function: %v\n", arr)
}
func main() {
a := [3]int{1, 2, 3}
modify(a)
fmt.Printf("Outside function: %v\n", a) // original array unchanged
}
Output:
Inside function: [999 2 3]
Outside function: [1 2 3]
▶ Example: Comparing Arrays (Only for Arrays of the Same Type and Length)
package main
import "fmt"
func main() {
a := [3]int{1, 2, 3}
b := [3]int{1, 2, 3}
c := [3]int{1, 2, 4}
fmt.Println(a == b) // true
fmt.Println(a == c) // false
// Items of different lengths or types cannot be compared.
// d := [4]int{1, 2, 3, 4}
// fmt.Println(a == d) // ❌ mismatched types
}
4. Four Ways to Create Slices
(1) A slice = a "view" of an array
graph TB
subgraph Slice["Slice (24 bytes)"]
P["ptr<br/>points to the underlying array"]
L["len<br/>length"]
C["cap<br/>capacity"]
end
subgraph Array["Underlying array"]
A0["[0]"]
A1["[1]"]
A2["[2]"]
A3["[3]"]
A4["[4]"]
A5["[5]"]
end
P --> A1
P -.-> A2
P -.-> A3
A slice is a reference to the underlying array—it consists of only 24 bytes (ptr + len + cap). Multiple slices can share the same underlying array.
(2) Creation Method 1: Directly declare a nil slice
var s []int // nil slice, len=0, cap=0
// You can append, but you cannot access it using `s[0]`.
(3) Creation Method 2: Literal
s := []int{1, 2, 3, 4, 5} // create and initialize
// Base array = [1 2 3 4 5], len = cap = 5
(4) Creation Method 3: make (specify len and cap)
// Syntax: make([]T, len, cap)
s := make([]int, 5) // len=5, cap=5, all zero values [0 0 0 0 0]
s := make([]int, 3, 10) // len=3, cap=10
▶ Example: Comparison of 4 ways to use "make"
package main
import "fmt"
func main() {
// Method 1: nil slice
var s1 []int
fmt.Printf("s1: len=%d cap=%d nil=%v\n", len(s1), cap(s1), s1 == nil)
// Method 2: Literal
s2 := []int{1, 2, 3}
fmt.Printf("s2: len=%d cap=%d %v\n", len(s2), cap(s2), s2)
// Method 3: Set the length
s3 := make([]int, 5)
fmt.Printf("s3: len=%d cap=%d %v\n", len(s3), cap(s3), s3)
// Method 4: make (length + capacity)
s4 := make([]int, 3, 10)
fmt.Printf("s4: len=%d cap=%d %v\n", len(s4), cap(s4), s4)
// Method 5: Slicing from an array
arr := [5]int{10, 20, 30, 40, 50}
s5 := arr[1:4] // [20 30 40], len=3 cap=4 (4 remaining elements)
fmt.Printf("s5: len=%d cap=%d %v\n", len(s5), cap(s5), s5)
}
Output:
s1: len=0 cap=0 nil=true
s2: len=3 cap=3 [1 2 3]
s3: len=5 cap=5 [0 0 0 0 0]
s4: len=3 cap=10 [0 0 0]
s5: len=3 cap=4 [20 30 40]
(6) Quick Reference: 4 Ways to Create
| Method | Syntax | Use Cases |
|---|---|---|
| nil slice | var s []T |
lazy initialization |
| Literal | []T{1, 2, 3} |
All elements known |
| make | make([]T, len, cap) |
Pre-allocate capacity |
| Array Slicing | arr[low:high] |
Subarray View |
5. len() and cap()
(1) The Relationship Between len and cap
package main
import "fmt"
func main() {
s := make([]int, 3, 10) // len=3, cap=10
fmt.Printf("Initial: len=%d cap=%d\n", len(s), cap(s))
// len=3: You can access s[0], s[1], and s[2]
// cap=10: The underlying array has 10 elements, with room for 7 more to be appended
}
(2) The Meaning of "cap"
cap - len = the number of elements that can still be appended without resizing. If this exceeds cap, append will allocate a new underlying مصفوفة (typically doubling the size).
▶ Example: The Actual Difference Between len and cap
package main
import "fmt"
func main() {
s := make([]int, 0, 5) // len=0, cap=5
for i := 1; i <= 8; i++ {
s = append(s, i)
fmt.Printf("append %d: len=%d cap=%d %v\n", i, len(s), cap(s), s)
}
}
Output:
append 1: len=1 cap=5 [1]
append 2: len=2 cap=5 [1 2]
append 3: len=3 cap=5 [1 2 3]
append 4: len=4 cap=5 [1 2 3 4]
append 5: len=5 cap=5 [1 2 3 4 5]
append 6: len=6 cap=10 [1 2 3 4 5 6] ← cap doubled
append 7: len=7 cap=10 [1 2 3 4 5 6 7]
append 8: len=8 cap=10 [1 2 3 4 5 6 7 8]
len == cap, the new cap is equal to the old cap * 2 (for small slices) or the old cap * 1.25 (for large slices, approximately > 256).
6. Append-based dynamic resizing
(1) Basics of append
package main
import "fmt"
func main() {
var s []int // nil
s = append(s, 1) // [1]
s = append(s, 2, 3, 4) // [1 2 3 4]
// Batch append: Splitting a slice
s2 := []int{5, 6, 7}
s = append(s, s2...) // [1 2 3 4 5 6 7]
fmt.Println(s)
}
Output:
[1 2 3 4 5 6 7]
▶ Example: Deleting elements using append (no built-in delete دالة)
package main
import "fmt"
// Delete the element at index i
func removeAt(s []int, i int) []int {
// Concatenation: s[:i] + s[i+1:]
return append(s[:i], s[i+1:]...)
}
func main() {
s := []int{1, 2, 3, 4, 5}
s = removeAt(s, 2) // delete 3
fmt.Println(s) // [1 2 4 5]
}
Output:
[1 2 4 5]
(3) The pitfall of append: It may modify the original array
package main
import "fmt"
func main() {
s := []int{1, 2, 3, 4, 5}
s2 := s[:3] // [1 2 3], shares underlying مصفوفة
s2 = append(s2, 99) // no resize, modified s's underlying مصفوفة!
fmt.Println("s:", s) // [1 2 3 99 5]
fmt.Println("s2:", s2) // [1 2 3 99]
}
Output:
s: [1 2 3 99 5]
s2: [1 2 3 99]
append may affect other slices. Use copy() or force resizing when strict isolation is required.
7. Slice [low:high:max]
(1) Three types of slice expressions
package main
import "fmt"
func main() {
arr := [5]int{10, 20, 30, 40, 50}
// Form 1: [low:high] — Takes [low, high)
s1 := arr[1:4]
fmt.Printf("arr[1:4] = %v, len=%d cap=%d\n", s1, len(s1), cap(s1))
// Format 2: [low:] — From low to the end
s2 := arr[2:]
fmt.Printf("arr[2:] = %v, len=%d cap=%d\n", s2, len(s2), cap(s2))
// Form 3: [:high] — From the beginning to high
s3 := arr[:3]
fmt.Printf("arr[:3] = %v, len=%d cap=%d\n", s3, len(s3), cap(s3))
// Format 4: [low:high:max] — Sets a cap (to prevent unexpected additions from affecting the original array)
s4 := arr[1:3:4]
fmt.Printf("arr[1:3:4] = %v, len=%d cap=%d\n", s4, len(s4), cap(s4))
}
Output:
arr[1:4] = [20 30 40], len=3 cap=4
arr[2:] = [30 40 50], len=3 cap=3
arr[:3] = [10 20 30], len=3 cap=5
arr[1:3:4] = [20 30], len=2 cap=3
(2) The Purpose of [low:high:max]
max sets a limit to prevent append from overwriting other parts of the original array:
package main
import "fmt"
func main() {
arr := [5]int{10, 20, 30, 40, 50}
// No limit on max: cap=4; after an append operation, arr[3] will be overwritten
s1 := arr[1:3] // [20 30], cap=4
s1 = append(s1, 999)
fmt.Println("arr:", arr) // [10 20 30 999 50] ← overwritten!
// Limit max=3: cap=2; append creates a new مصفوفة
s2 := arr[1:3:3] // [20 30], cap=2
s2 = append(s2, 888)
fmt.Println("arr:", arr) // [10 20 30 999 50] ← unchanged
fmt.Println("s2:", s2) // [20 30 888]
}
Output:
arr: [10 20 30 999 50]
arr: [10 20 30 999 50]
s2: [20 30 888]
8. Pitfalls of Sharing the Underlying Array in Slicing (Key Point)
(1) Pitfall 1: Modifying a slice element affects all slices
package main
import "fmt"
func main() {
arr := [5]int{1, 2, 3, 4, 5}
s1 := arr[0:3] // [1 2 3]
s2 := arr[2:5] // [3 4 5]
s1[2] = 999 // same underlying array!
fmt.Println("s1:", s1) // [1 2 999]
fmt.Println("s2:", s2) // [999 4 5]
fmt.Println("arr:", arr) // [1 2 999 4 5]
}
(2) Pitfall 2: Using append with for range causes an infinite loop
package main
import "fmt"
func main() {
s := []int{1, 2, 3}
for _, v := range s {
s = append(s, v*10) // dangerous: s is growing
}
fmt.Println(s) // output is غير معرّف
}
for range loop. Prior to Go 1.22, s in for i, v := range s was a snapshot of the length at the start of the loop; Go 1.22 fixed this in some scenarios, but caution is still advised.
(3) Pitfall 3: Slices passed as function arguments are unexpectedly modified
package main
import "fmt"
// Slicing uses pass-by-reference, so changes made within the function affect the outside.
func addElement(s []int) {
s = append(s, 100) // may modify the original array (if no resize)
fmt.Printf("Inside function: %v\n", s)
}
func main() {
s := make([]int, 3, 10)
s[0], s[1], s[2] = 1, 2, 3
addElement(s)
fmt.Printf("Outside function: %v\n", s) // depends on whether resize occurred
}
(4) Use copy() to resolve sharing issues
package main
import "fmt"
func main() {
arr := [5]int{1, 2, 3, 4, 5}
s1 := arr[0:3]
// Create a separate copy
s2 := make([]int, len(s1))
copy(s2, s1)
s1[0] = 999
fmt.Println("s1:", s1) // [999 2 3]
fmt.Println("s2:", s2) // [1 2 3] ← unaffected
}
▶ Example: Using "copy + append" to insert a slice
package main
import "fmt"
// Insert element val at index i
func insert(s []int, i int, val int) []int {
// 1. Add 1 position
s = append(s, 0)
// 2. Shift the element at index i and all subsequent elements one position to the right
copy(s[i+1:], s[i:])
// 3. Add a new element
s[i] = val
return s
}
func main() {
s := []int{1, 2, 4, 5}
s = insert(s, 2, 3)
fmt.Println(s) // [1 2 3 4 5]
}
Output:
[1 2 3 4 5]
9. Complete Example: Dynamic Data Collector
Combine all the features of the slices to build a tool for multi-source data collection + deduplication + filtering:
// collector.go
package main
import (
"fmt"
"strings"
)
// Data Source Simulation Functions
func fetchFromSource(sourceName string) []string {
data := map[string][]string{
"GitHub": {"repo:go", "repo:docker", "issue:bug", "repo:go"}, // last one is duplicate
"NPM": {"pkg:react", "pkg:vue", "pkg:react"}, // duplicate
"PyPI": {"pkg:requests", "pkg:flask"},
}
return data[sourceName]
}
// Removing Duplicates: Using `map` to Create a Hash Set
func deduplicate(data []string) []string {
seen := make(map[string]bool)
result := make([]string, 0, len(data))
for _, item := range data {
if !seen[item] {
seen[item] = true
result = append(result, item)
}
}
return result
}
// Filter: Keep only those starting with "repo:"
func filterRepos(data []string) []string {
result := make([]string, 0)
for _, item := range data {
if strings.HasPrefix(item, "repo:") {
result = append(result, item)
}
}
return result
}
// Pipeline: Combining Multiple Steps
func collectPipeline() (total, unique, repos int, sources []string) {
defer func() {
// Naming Return Values + Defer Collection Statistics
fmt.Printf("\n[Collection Complete] total=%d unique=%d repos=%d sources=%v\n",
total, unique, repos, sources)
}()
allSources := []string{"GitHub", "NPM", "PyPI"}
var allData []string
for _, src := range allSources {
sources = append(sources, src)
data := fetchFromSource(src)
allData = append(allData, data...)
total += len(data)
}
uniqueData := deduplicate(allData)
unique = len(uniqueData)
repoData := filterRepos(uniqueData)
repos = len(repoData)
fmt.Println("\n=== Deduped Data ===")
for _, item := range uniqueData {
fmt.Printf(" %s\n", item)
}
fmt.Println("\n=== Filtering Repo Data ===")
for _, item := range repoData {
fmt.Printf(" %s\n", item)
}
return total, unique, repos, sources
}
func main() {
collectPipeline()
}
Expected Output:
=== Deduped Data ===
repo:go
repo:docker
issue:bug
pkg:react
pkg:vue
pkg:requests
pkg:flask
=== Filtering Repo Data ===
repo:go
repo:docker
[Collection Complete] total=9 unique=7 repos=2 sources=[GitHub NPM PyPI]
make([]string, 0, len(data)) to pre-allocate capacity and avoid frequent resizing with append. This is a key performance optimization technique.
❓ FAQ
append always cause the array to resize?len == cap. If there is still space available, the elements are written directly to the original array.append affects the external slice depends on whether the slice is resized.max parameter in the [low:high:max] format required?append operations from affecting other elements in the original array. Common scenario: creating a sub-slice from a larger slice followed by an append operation.nil slice and an empty slice?nil slice (such as var s []int) evaluates to true when checked for nil; its len is 0 and its cap is 0. An empty slice (such as []int{} or make([]int, 0)) evaluates to false when compared to nil, with len=0 and cap=0. Both can be appended to, but the JSON serialization results differ: nil → null, empty → [].list.remove(i). Go implements removal using append(s[:i], s[i+1:]...) (but this does not preserve the order). If you want to preserve the order, you need to manually reorder the elements.copy(dst, src) — Copies min(len(dst), len(src)) elements; (2) append([]T(nil), src...) — Copies all elements; (3) Manually using for range.range?range on an array copies the entire array (poor performance); using range on a slice copies only 24 bytes (good performance). This is why slices are the de facto standard for "collections."📖 Summary
- Arrays are value types (fixed-length, passed by copy), while slices are reference types (dynamic, passed by sharing the underlying data).
- 4 Ways to Create Slices: nil / literals / make / array slices
lenis the number of visible elements,capis the capacity of the underlying array, andappendresizes the array whenlen == cap(doubling strategy)- The three-parameter slice expression
[low:high:max]sets a limit oncapto prevent out-of-bounds appends - Sharing the underlying array in slices leads to three classic pitfalls: modifications to elements affecting each other / appending within a
range/ side effects of function-based appending - Solutions:
copy()to create a separate copy / force resizing (append(s, 0)[:0]) / limit the cap - In actual projects, 99% of the time slices are used; arrays are only used in scenarios involving fixed sizes or value semantics.
📝 Exercises
-
Basic Problem (Difficulty ⭐): Use slices to implement a
reverse([]int) []intfunction that reverses the elements of a slice. Hint: Use aforloop with a temporary variable, or swap elements in-place. Test case:reverse([]int{1,2,3,4,5})→[5 4 3 2 1]. -
Advanced Problem (Difficulty ⭐⭐): Implement a
uniqueSorted(nums []int) []intfunction: remove duplicates and sort in ascending order. Hint: First sort the array usingsort.Ints, then iterate through it to remove duplicates. Given the input[]int{3, 1, 4, 1, 5, 9, 2, 6, 5}, the output should be[1 2 3 4 5 6 9]. -
Challenge Problem (Difficulty ⭐⭐⭐): Implement a sliding window maximum algorithm:
maxSlidingWindow(nums []int, k int) []int. For example,nums=[1,3,-1,-3,5,3,6,7], k=3→ Output[3,3,5,5,6,7]. Requirements: (1) Time complexity O(n); (2) Use adeque(which can be simulated using[]int) to maintain the window; (3) Explain why a direct traversal would result in a timeout (O(n*k)).