This is how you use slices in Go

5 min read
zen8labs slice in Go

In Go, slices are one of the most important data structures, providing developers with a way to work with and manage collections of data similar to what we use at zen8labs. In this blog, I will introduce some internal aspects of slices and highlight some pitfalls to avoid when using slices in Go.

What is a slice?

A slice is a dynamic array, meaning you can extend and shrink its length as needed, whereas arrays in Go are fixed-length data types. Moreover, slices offer all the benefits of indexing, iteration, and garbage collection optimizations because the underlying memory is allocated in sequential blocks. 

Slices are objects that reference an underlying array. Go requires three pieces of metadata to initialize a slice: a pointer to the underlying array, the length of the elements the slice can access, and the capacity, which indicates the number of elements the slice can accommodate for growth. By using the built-in functions len(<slice_here>) and cap(<slice_here>), we can inspect the length and capacity information of the slice. 

1. Slice internals 

zen8labs slice in Go 1

Firstly, let’s examine the header value of a slice through the snippet code below: 

slice := []int{1, 2, 3} 
first_element := &slice[0] 
fmt.Printf("%p\\n", slice) // 0xc000198000 
fmt.Printf("%p", first_element) // 0xc000198000

=> Header value of a slice point to first element of an underlying array 

2. Slicing a slice 

When working with slices, understanding the correlation between the length and capacity of a slice helps us avoid some bugs related to slicing a slice. 

// slice's length = 5 and slice's capacity = 5. 
slice := []int{1, 2, 3, 4, 5} 

// slicing 
newSlice := slice[1:3]

Here, we have two slices that share the same underlying array. However, each slice views the underlying array differently. 

zen8labs slice in Go 2

As you can see, the length of newSlice is 2 and the capacity is 4. Can you guess how the length and capacity of a slice are calculated? Let’s check the example below: 

// slice's length = 5 and slice's capacity = 5. 
slice := []int{1, 2, 3, 4, 5} 

// slicing 
newSlice := slice[1:3] // i = 1 and j = 3 

// For newSlice[i:j] with an underlying array of capacity k 
// Length:  j - i = 3 - 1 = 2 
// Capacity: k - i = 5 - 1 = 4 
fmt.Println(len(slice) == 2) // true 
fmt.Println(cap(slice) == 4) // true

Now, you know that two slices share the same underlying array. Changing elements in one slice can affect the other slices. 

slice := []int{1, 2, 3, 4, 5} 

newSlice := slice[1:3] 
newSlice[1] = 8 

fmt.Println(slice)    // [1 2 8 4 5] 
fmt.Println(newSlice) // [2  8]
slice in go zen8labs 3

3. Growing slices 

Growing slices uses an append function. The append function will return a new slice with the changes. The length of a new slice is increased, and the capacity may or may not be affected, depending on the available capacity of the original slice. 

The append operation is a smart function for increasing the capacity of the underlying array. The capacity is doubled whenever the existing capacity of the slice is below 1,000 elements. Once the number of elements exceeds 1,000, the capacity increases by a factor of 1.25, or 25%. This growth algorithm may change in the language over time. 

3.1. Length equals to capacity 

slice := []int{1, 2, 3} 
newSlice := append(slice, 4) 

fmt.Println(slice)    // [1 2 3]
fmt.Println(newSlice) // [1 2 3 4] 

firstElementOfSlice := slice[0] 
firstElementOfNewSlice := newSlice[0] 

fmt.Println(firstElementOfSlice)    // 1  
fmt.Println(firstElementOfNewSlice) // 1 

fmt.Println(&firstElementOfSlice)     // 0xc00018e048 
fmt.Println(&firstElementOfNewSlice)  // 0xc00018e060

When the underlying array of a slice lacks available capacity, the append function generates a new underlying array. It then copies the existing referenced values and assigns the new value. 

zen8labs slice in Go 4

After this append operation, newSlice is given its own underlying array, and the capacity of the array is doubled from its original size. 

3.2. Length less than capacity

slice := []int{1, 2, 3, 4, 5} 

newSlice := slice[1:3] 

newSlice = append(newSlice, 6)
zen8labs slice in Go 5

As you can observe, when a new element is appended to newSlice, the third element in the original slice is updated. It’s crucial to take note of this behavior, especially when you share a slice across multiple parts of your program, unless you explicitly intend to modify it. 

To prevent impacting other slices that share the same underlying array, you can go back to scenario of 1. Length equals capacity. This scenario shows that when appending a new element to a slice, it will create its own underlying array. But how can you achieve this? Don’t worry, Go supports the three-index slice when slicing a slice. 

// slice's length = 5 and slice's capacity = 5. 
slice := []int{1, 2, 3, 4, 5} 

// slicing 
newSlice := slice[2:3:3] 

// For newSlice[i:j:k] or [2:3:3] 
// Length: j - i = 3 - 2 = 1 
// Capacity: k - i = 3 - 2 = 1 
fmt.Println(len(newSlice) == 1) // true 
fmt.Println(cap(newSlice) == 1) // true 

// Append a new element to the slice. 
newSlice = append(newSlice, 6)
slice in go zen8labs

4. Passing slices between functions 

Passing a slice into a function means passing a copy of the header value of the original slice but it shares the same underlying data. 

slice := make([]int, 1000) 
slice = add(slice) 

func add(slice []int) []int { 
      ... 
      return slice 

}
slice in go zen8labs 7

Up to this point, you’ve learned that passing a slice can be more efficient than passing an array into a function. This is because passing a slice only involves creating a new header value that points to the same underlying array as the original slice, without duplicating the underlying data (which would be an inefficient operation). 

Conclusion

Overall, slices play a pivotal role in Go programming, offering flexibility, efficiency, and ease of use in managing and manipulating data collections. By mastering slice manipulation techniques, developers can build more robust and scalable applications in Go (reference: Go in Action by William Kennedy). If you want to learn about all areas IT related, then reach out to us at zen8labs!

Tung Vu, Software Engineer

Related posts

Odoo is a comprehensive framework that allows developers to create niche items for their projects. This blog gives you a guide to Odoo and how to succeed.
7 min read
When selecting a technology stack for a new project, developers often need substantial time to evaluate various options. The process from initiation to achieving a stable codebase can be daunting, especially for projects requiring rapid development. With Supabase, zen8labs' team has managed to expedite this process while providing scalability and stability for our projects.
5 min read
Carrying out the right performance test can help any IT project succeed. This blog shows how K6 can help you carry out the most common of performance tests.
5 min read