Post

05. Arrays and Slices

๐Ÿค” Delve into the world of Go arrays and slices! This guide unlocks the fundamentals, from creation to advanced operations, empowering you to efficiently manage data structures in Go. ๐Ÿš€

05. Arrays and Slices

What we will learn in this post?

  • ๐Ÿ‘‰ Arrays in Go
  • ๐Ÿ‘‰ Slices Fundamentals
  • ๐Ÿ‘‰ Creating Slices
  • ๐Ÿ‘‰ Slice Operations
  • ๐Ÿ‘‰ Slice Internals
  • ๐Ÿ‘‰ Multi-dimensional Slices
  • ๐Ÿ‘‰ Conclusion!

Go Arrays ๐Ÿš€

Arrays in Go are like fixed-size containers for storing elements of the same type. Think of them as a row of boxes, where each box holds a value.

Declaring and Initializing

You declare an array by specifying its size and the data type it will hold. For example:

1
var numbers [5]int // An array of 5 integers

You can initialize it when declaring:

1
numbers := [5]int{10, 20, 30, 40, 50}

Or, you can let Go infer the size:

1
numbers := [...]int{10, 20, 30, 40, 50} // Size is determined by the number of elements

Zero Values and Accessing Elements

If you donโ€™t initialize the array, elements are assigned their zero value (0 for integers, โ€œโ€ for strings, etc.).

1
var names [3]string // Defaults to ["", "", ""]

Access elements using their index (starting from 0):

1
2
fmt.Println(numbers[0]) // Output: 10
numbers[1] = 25       // Changing the value at index 1

Key Points to Remember ๐Ÿค”

  • Fixed Size: Once declared, the size of an array cannot be changed.
  • Value Type: When you assign one array to another, a copy is created. Changes to the new array wonโ€™t affect the original.
1
2
3
4
arr1 := [2]int{1, 2}
arr2 := arr1 // arr2 is a copy of arr1
arr2[0] = 5 // Modifying arr2
fmt.Println(arr1, arr2) // Output: [1 2] [5 2]

To learn more:

Slices: Your Dynamic Array Windows in Go ๐ŸชŸ

// Practical Example: Using arrays for sensor data

1
2
3
4
5
6
7
8
9
package main
import "fmt"
func main() {
    var temperatures [7]float64 // Weekly temperature readings
    temperatures[0] = 22.5
    temperatures[1] = 23.0
    // ...
    fmt.Println("Monday's temperature:", temperatures[0])
}

Slices in Go are like flexible windows into an underlying array. They let you work with portions of an array without copying the data. Think of them as a dynamic view ๐Ÿ˜ฎ.

// Practical Example: Array for error codes

1
2
var errorCodes = [3]int{404, 500, 403}
fmt.Println("First error code:", errorCodes[0])

Slices vs. Arrays: Whatโ€™s the Difference? ๐Ÿง

Hereโ€™s the lowdown: // Practical Example: Array copy for backup

1
2
3
4
original := [2]string{"active", "pending"}
backup := original
backup[1] = "archived"
fmt.Println("Original:", original, "Backup:", backup)
  • Arrays:
    • Fixed size. You define the size when you declare them, and it canโ€™t change.
    • Value type. When you assign one array to another, you get a completely new copy.
  • Slices: // Practical Example: Slices for user input
1
2
3
users := []string{"Alice", "Bob"}
users = append(users, "Charlie")
fmt.Println("All users:", users)

Dynamic size. Slices can grow or shrink, making them more adaptable. * Reference type. Slices donโ€™t store the data directly; they hold a pointer to a section of an underlying array. Assigning a slice copies the pointer, not the data itself.

// Practical Example: Slices for log entries

1
2
3
logs := []string{}
logs = append(logs, "Started server", "Received request")
fmt.Println("Logs:", logs)

Why Slices Rule ๐Ÿ‘‘

Slices are generally preferred in Go because of their flexibility. You rarely use arrays directly. Check out this resource for more details: Go Slices: usage and internals.

1
2
3
4
5
6
7
// Array (fixed size)
var myArray [3]int = [3]int{1, 2, 3}

// Slice (dynamic size)
mySlice := []int{1, 2, 3} // Creates a slice backed by an anonymous array
mySlice = append(mySlice, 4) // Adding an element to the slice
println(len(mySlice))

// Practical Example: Slice for product prices

1
2
prices := []float64{9.99, 14.99, 7.50}
fmt.Println("Prices:", prices)

// Practical Example: Pre-allocating slice for batch processing

1
2
3
4
5
batch := make([]int, 0, 100)
for i := 0; i < 50; i++ {
    batch = append(batch, i)
}
fmt.Println("Batch size:", len(batch))

// Practical Example: Slicing for pagination

1
2
3
allItems := []string{"A", "B", "C", "D", "E", "F"}
page := allItems[2:5]
fmt.Println("Page items:", page)

Slices make it easier to work with collections of data that change over time โฑ๏ธ.

// Practical Example: Variadic function using slices

1
2
3
4
5
6
7
8
func sum(nums ...int) int {
    total := 0
    for _, n := range nums {
        total += n
    }
    return total
}
fmt.Println("Sum:", sum(1, 2, 3, 4))

Letโ€™s explore how to create slices in Go! ๐Ÿš€

Creating Slices in Go

// Practical Example: Monitoring slice capacity

1
2
3
4
5
data := make([]int, 0, 2)
for i := 0; i < 5; i++ {
    data = append(data, i)
    fmt.Printf("Len: %d, Cap: %d\n", len(data), cap(data))
}

Slices are dynamic arrays in Go, offering flexibility. Here are the common ways to create them:

Slice Literals

Imagine an array: [A, B, C, D, E, F].

Below is a visual diagram showing the flow of slice creation and usage in Go. This helps clarify how slices reference underlying arrays and how operations are performed:

graph LR
    FD_DECL["Function Declaration"]:::javaStyle --> FD_CALL["Function Call"]:::jdbcStyle
    FD_CALL --> FD_EXEC{"Function Body Execution"}:::driverStyle
    FD_EXEC --> FD_RET["Return Value"]:::dbStyle
    FD_RET --> FD_USE["Using Return Value"]:::useStyle

    classDef javaStyle fill:#ff4f81,stroke:#c43e3e,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
    classDef jdbcStyle fill:#6b5bff,stroke:#4a3f6b,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
    classDef driverStyle fill:#ffd700,stroke:#d99120,color:#222,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
    classDef dbStyle fill:#00bfae,stroke:#005f99,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
    classDef useStyle fill:#ff9800,stroke:#f57c00,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;

    class FD_DECL javaStyle;
    class FD_CALL jdbcStyle;
    class FD_EXEC driverStyle;
    class FD_RET dbStyle;
    class FD_USE useStyle;

    linkStyle default stroke:#e67e22,stroke-width:3px;
1
2
mySlice := make([]int, 5, 10) // Creates a slice of ints, length 5, capacity 10.
// Resources: https://go.dev/ref/spec#Making_slices_maps_and_channels

// Practical Example: Slicing for pagination

1
2
matrix := [][]int{{1, 2, 3}, {4, 5, 6}, {7, 8, 9}}

Choosing the right method depends on your needs: literals for known values, make for controlled size and capacity, and slicing for creating views of existing data.

// Practical Example: Named return value for slice modification

1
2
3
4
5
6
7
    func updateFirst(slice []int, value int) (updated []int) {
        slice[0] = value
        updated = slice
        return
    }
    nums := []int{1, 2, 3}
    fmt.Println("Updated slice:", updateFirst(nums, 99))

Working with Slices in Go ๐Ÿš€

Letโ€™s explore some common and super useful slice operations in Go! Slices are like flexible arrays. // Practical Example: Defer with slices


func process(items []string) {
    defer fmt.Println("Processing complete!")
    for _, item := range items {
        fmt.Println("Item:", item)
    }
}
process([]string{"A", "B", "C"})

Essential Slice Actions ๐Ÿ› ๏ธ

  • Adding Elements: append() grows your slice.

    1
    2
    
    nums := []int{1, 2, 3}
    nums = append(nums, 4, 5) // nums is now [1 2 3 4 5]
    

// Practical Example: Closure for matrix row sum

1
2
3
4
5
6
7
8
9
matrix := [][]int{{1, 2, 3}, {4, 5, 6}, {7, 8, 9}}
rowSum := func(row []int) int {
    sum := 0
    for _, v := range row {
        sum += v
    }
    return sum
}
fmt.Println("Sum of first row:", rowSum(matrix[0]))

// Practical Example: Higher-order function for matrix processing

1
2
3
4
5
6
7
8
9
10
11
func applyToMatrix(matrix [][]int, fn func(int) int) [][]int {
    for i := range matrix {
        for j := range matrix[i] {
            matrix[i][j] = fn(matrix[i][j])
        }
    }
    return matrix
}
double := func(x int) int { return x * 2 }
result := applyToMatrix(matrix, double)
fmt.Println("Doubled matrix:", result)
  • Duplicating: copy() creates a new slice with the same content.

    1
    2
    3
    
    original := []int{10, 20}
    duplicate := make([]int, len(original))
    copy(duplicate, original) // duplicate is now [10 20]
    
  • Checking Size: len() gives you the number of elements. cap() shows the sliceโ€™s capacity (total space allocated).

    1
    2
    3
    
    mySlice := []string{"a", "b"}
    fmt.Println(len(mySlice)) // Output: 2
    fmt.Println(cap(mySlice)) // Output: usually 2 (or more, depends on creation)
    
  • Slicing: [start:end] creates a sub-slice.

    1
    2
    
    letters := []string{"A", "B", "C", "D", "E"}
    sub := letters[1:4] // sub is now ["B" "C" "D"]
    

Slice Growth and Memory ๐Ÿง 

Slices grow dynamically! When you append() beyond capacity, Go reallocates a new, bigger underlying array, copies the old data, and then adds the new element. This can be performance-intensive. To minimize reallocations, you can use make() to create a slice with an initial capacity if you know how large it will grow. Tip: Avoid appending in loops if you can pre-allocate!

Hereโ€™s an explanation of Go slices with diagrams, tailored for easy understanding:

Understanding Go Slices: A Visual Guide ๐Ÿš€

Go slices are powerful! Think of them like flexible views into an array. Internally, they hold three things:

  • A pointer โžก๏ธ to the starting element of the underlying array.
  • A length ๐Ÿ“ representing the number of elements the slice currently holds.
  • A capacity ๐Ÿ“ฆ indicating the total space available in the underlying array from the sliceโ€™s starting point.

How Slices Work ๐Ÿ› ๏ธ

Imagine an array: [A, B, C, D, E, F].

graph LR
subgraph Array
    A[A] --> B[B]
    B --> C[C]
    C --> D[D]
    D --> E[E]
    E --> F[F]
end

A slice mySlice := myArray[1:4] (taking elements from index 1 up to, but not including, 4) would:

  • Point to element B.
  • Have a length of 3 (B, C, D).
  • Have a capacity of 5 (B, C, D, E, F).
graph LR
subgraph Array
    A[A] --> B[B]
    B --> C[C]
    C --> D[D]
    D --> E[E]
    E --> F[F]
end

subgraph Slice
    Pointer --> B
    Length --> 3
    Capacity --> 5
end

Modifications and Array Allocations โš™๏ธ

Modifying mySlice can affect the underlying array and other slices that share it. For example:

1
2
3
myArray := [6]string{"A", "B", "C", "D", "E", "F"}
mySlice := myArray[1:4] // mySlice is [B, C, D]
mySlice[0] = "X"        // Changes myArray[1] to "X"

If you append to mySlice and its length is less than its capacity, the underlying array is used. If the length would exceed the capacity, a new, larger array is allocated, the data is copied, and the slice pointer is updated. The original array remains unchanged!

1
mySlice = append(mySlice, "G") // If capacity is enough, no new array

For more in-depth information, refer to the official Go blog on slices: Go Slices: usage and internals.

Okay, letโ€™s explore multi-dimensional slices in Go! ๐Ÿš€

Slices of Slices in Go: Multi-Dimensional Arrays

Go doesnโ€™t have built-in multi-dimensional arrays like some languages. Instead, you use slices of slices. Think of it as a slice where each element is another slice.

Creating 2D Slices (Matrices)

You can initialize them in a few ways:

  • Directly:

    {% raw %}

    1
    
    matrix := [][]int{{1, 2, 3}, {4, 5, 6}, {7, 8, 9}}
    
  • Using make:

    1
    2
    3
    4
    5
    6
    
    rows := 3
    cols := 4
    matrix := make([][]int, rows) // Create 'rows' number of rows
    for i := range matrix {
        matrix[i] = make([]int, cols) // Initialize each row with 'cols' elements
    }
    

Accessing Elements

Accessing elements is straightforward: matrix[row][column]. For example, matrix[0][1] would access the element at row 0, column 1 (which is 2 in our first example).

Hereโ€™s the breakdown:

  • matrix[0] gives you the first []int (the first row).
  • [1] then picks the second element from that slice.

Example

1
2
3
4
5
6
7
8
9
10
11
```go
package main

import "fmt"

func main() {
 matrix := [][]int{{1, 2, 3}, {4, 5, 6}, {7, 8, 9}}

 fmt.Println(matrix[1][2]) // Output: 6
}

```

For more in depth explanation:

Conclusion

Fantastic! We made it! ๐Ÿฅณ Now itโ€™s your chance to chime in. What are your biggest takeaways? Any questions? Leave a comment below and letโ€™s discuss! Iโ€™m excited to hear what you have to say! ๐Ÿ‘‡๐Ÿ˜Š

This post is licensed under CC BY 4.0 by the author.