Slice Notions You Should Know in Golang

Mastering Slice Operations: Tips and Tricks

There is nothing so useless as doing efficiently that which should not be done at all.
— Peter Drucker

Medium Link: Slice Notions You Should Know in Golang | by Wesley Wei | Jul, 2024 | Programmer’s Career

Hello, here is Wesley, Today’s article is about slice in Go. Without further ado, let’s get started.💪

1. Array vs Slice

1.1 Declaration and Initialization

An array is a collection of elements of the same data type, and it needs to specify its length and element type when declared. It cannot dynamically expand, and its size will be determined at compile time.

1
2
3
4
5
6
7
8
func main() {
var arrays [3]int // Declare and initialize with default zero values
var arrays1 = [4]int{1, 2, 3, 4} // Declare and initialize simultaneously
var arrays2 = […]int{1, 2, 3, 4, 5} // … can represent the length of the initialization value
fmt.Println(arrays) // [0 0 0]
fmt.Println(arrays1) // [1 2 3 4]
fmt.Println(arrays2) // [1 2 3 4 5]
}

Arrays have limited use cases, and slices are more commonly used. A slice is a variable-length sequence of elements of the same type, which is based on an array type and provides a layer of abstraction. It is very flexible and supports automatic expansion.

1
2
3
4
5
type slice struct {
array unsafe.Pointer
len int
cap int
}

image.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func main() {
var slice []int // Declare directly
fmt.Println(len(slice), cap(slice)) // 0 0
slice1 := []int{1, 2, 3, 4} // Literal way
fmt.Println(len(slice1), cap(slice1)) // 4 4
slice2 := make([]int, 3, 5) // Use the make function to construct a slice
fmt.Println(len(slice2), cap(slice2)) // 3 5

slice3 := append(slice1, 1)
fmt.Println(len(slice1), cap(slice1)) // 4 4
fmt.Println(len(slice3), cap(slice3)) // 5 8
slice4 := slice3[1:5]
fmt.Println(len(slice4), cap(slice4), slice4) // 4 7 [2 3 4 1]
}

You may be curious about the output of slice3 and slice4. This involves the expansion strategy, which will be discussed below.

1.2 Function Parameters

In Go, only value copying is used, so if you pass an array to a function and modify its elements in the function, it will not affect the original array.

However, slices are different. As mentioned earlier, when you pass a slice to a function, you are actually passing a copy of the array pointer, as well as the length and capacity. The pointer may point to the same array, so modifying the elements in the function may affect the original array.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func modifySlice(s []string) {
s[0] = "tfrain"
s[1] = "github"
fmt.Println("modifySlice slice:", s)
}

func main() {
s := []string{"wesleywei", "medium"}
fmt.Println("main slice:", s)
modifySlice(s)
fmt.Println("main slice:", s)
}
// main slice: [wesleywei medium]
// modifySlice slice: [tfrain github]
// main slice: [tfrain github]

Of course, the word I use here is ’may‘, and the examples I give are ones that affect the original array. But if you change the pointer to the underlying array inside the function, such as expansion or copying, it will not affect the external original array.

The following sections will introduce expansion scenarios and distinguish between these situations. Please continue reading. Some complex? Or perhaps this is the price for slice’s greater flexibility and broader application scenarios.

2. Copying Large Slices or Small Slices

In the Go language, there is only value passing, as mentioned earlier:

1
2
3
4
5
type slice struct {
array unsafe.Pointer
len int
cap int
}

If a copy occurs, it essentially copies these three fields. The difference between large slices and small slices is that the values of len and cap are slightly larger for large slices, so the cost is similar.

3. Shallow and Deep Copying of Slices

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
func main() {
slice1 := []int{1, 2, 3, 4}
arrayPtr1 := (*int)(unsafe.Pointer(&slice1[0]))
fmt.Printf("The address of the underlying array of slice1: %p\n", arrayPtr1)

slice2 := slice1
arrayPtr2 := (*int)(unsafe.Pointer(&slice2[0]))
fmt.Printf("The address of the underlying array of slice2: %p\n", arrayPtr2)

slice3 := slice2[:]
arrayPtr3 := (*int)(unsafe.Pointer(&slice3[0]))
fmt.Printf("The address of the underlying array of slice3: %p\n", arrayPtr3)

slice4 := make([]int, len(slice3))
copy(slice4, slice3)
arrayPtr4 := (*int)(unsafe.Pointer(&slice4[0]))
fmt.Printf("The address of the underlying array of slice4: %p\n", arrayPtr4)
}

// The address of the underlying array of slice1: 0xc00007a000
// The address of the underlying array of slice2: 0xc00007a000
// The address of the underlying array of slice3: 0xc00007a000
// The address of the underlying array of slice4: 0xc00007a020

The examples above show that:

  • Using :=or = operator to copy a slice is shallow copying.
  • Using [:] indexing to copy a slice is also shallow copying.
  • Using Go’s built-in copy() function to copy a slice is deep copying.

LeetCode 47 can help you understand the use of copy():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
func permute(nums []int) [][]int {
var res [][]int
n := len(nums)
visited := make([]bool, n)
var build func(subs []int)
build = func(subs []int) {
if len(subs) == n {
tmp := make([]int, n)
copy(tmp, subs)
res = append(res, tmp)
}
for i := 0; i < n; i++ {
if visited[i] {
continue
}
visited[i] = true
build(append(subs, nums[i]))
visited[i] = false
}
}
build(nil)
return res
}

4. Slice Expansion Strategy

go1.20 slice

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

newcap := oldCap
doublecap := newcap + newcap
if newLen > doublecap {
newcap = newLen
} else {
const threshold = 256
if oldCap < threshold {
newcap = doublecap
} else {
// Check 0 < newcap to detect overflow
// and prevent an infinite loop.
for 0 < newcap && newcap < newLen {
// Transition from growing 2x for small slices
// to growing 1.25x for large slices. This formula
// gives a smooth-ish transition between the two.
newcap += (newcap + 3*threshold) / 4
}
// Set newcap to the requested cap when
// the newcap calculation overflowed.
if newcap <= 0 {
newcap = newLen
}
}
}

  • The capacity of a new slice is at least twice or 1.25 times the old slice’s capacity, depending on whether the original slice’s capacity is less than 256 or not.
  • In the later part of the source code, when expanding a slice, it will perform memory alignment, which is related to memory allocation strategies and is relatively complex, so we’ll ignore it for now.
  • After expansion, the underlying array of the slice has changed.

5. Nil Slice, Empty Slice, Zero Slice

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
package main

import (
"fmt"
"reflect"
"unsafe"
)

func main() {
// nil slice
var nilSlice []int

// empty slice
emptySlice := []int{}
emptySlice2 := make([]int, 0)

fmt.Printf("nilSlice: len=%d, cap=%d, is nil: %t\n", len(nilSlice), cap(nilSlice), nilSlice == nil)
fmt.Printf("emptySlice: len=%d, cap=%d, is nil: %t\n", len(emptySlice), cap(emptySlice), emptySlice == nil)
fmt.Printf("emptySlice2: len=%d, cap=%d, is nil: %t\n", len(emptySlice2), cap(emptySlice2), emptySlice2 == nil)

nilSliceHeader := (*reflect.SliceHeader)(unsafe.Pointer(&nilSlice))
emptySliceHeader := (*reflect.SliceHeader)(unsafe.Pointer(&emptySlice))
emptySlice2Header := (*reflect.SliceHeader)(unsafe.Pointer(&emptySlice2))

fmt.Printf("Pointer of nilSlice: %p\n", unsafe.Pointer(nilSliceHeader.Data))
fmt.Printf("Pointer of emptySlice: %p\n", unsafe.Pointer(emptySliceHeader.Data))
fmt.Printf("Pointer of emptySlice2: %p\n", unsafe.Pointer(emptySlice2Header.Data))

if emptySliceHeader.Data == emptySlice2Header.Data {
fmt.Println("emptySlice and emptySlice2 point to the same zerobase address.")
} else {
fmt.Println("emptySlice and emptySlice2 do not point to the same zerobase address.")
}
}

// nilSlice: len=0, cap=0, is nil: true
// emptySlice: len=0, cap=0, is nil: false
// emptySlice2: len=0, cap=0, is nil: false
// Pointer of nilSlice: 0x0
// Pointer of emptySlice: 0x58f360
// Pointer of emptySlice2: 0x58f360
// emptySlice and emptySlice2 point to the same zerobase address.
  • A nil slice has a length and capacity of 0, and its equality comparison with nil returns true.
  • An empty slice also has a length and capacity of 0, but its equality comparison with nil returns false, because all empty slices’ data pointers point to the zerobase address.
1
2
// base address for all 0-byte allocations
var zerobase uintptr

go1.20 zerobase

1
2
slice := make([]int, 5) // 0 0 0 0 0
slice := make([]*int, 5) // nil nil nil nil nil
  • A zero slice has its underlying array elements all set to zero values or nil, and a slice created with make that has a length and capacity not equal to 0 is considered a zero slice.

6. Passing a Slice vs Passing a Pointer to a Slice

Let’s look at an example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
package main

import (
"fmt"
"unsafe"
)

func modifySlice(s []int) {
s[0] = 100
s = append(s, 200)
fmt.Println("Inside modifySlice (modified slice):", s)
arrayPtr := (*int)(unsafe.Pointer(&s[0]))
fmt.Printf("The address of the underlying array in modifySlice: %p\n", arrayPtr)
}

func modifySlicePointer(s *[]int) {
(*s)[0] = 100
*s = append(*s, 200)
fmt.Println("Inside modifySlicePointer (modified slice pointer):", *s)
arrayPtr := (*int)(unsafe.Pointer(&(*s)[0]))
fmt.Printf("The address of the underlying array in modifySlicePointer: %p\n", arrayPtr)
}

func main() {
originalSlice := make([]int, 3, 4)
originalSlice[0], originalSlice[1], originalSlice[2] = 0, 1, 2
// originalSlice := []int{0, 1, 2}

fmt.Println("Original slice before modifySlice:", originalSlice)
arrayPtr := (*int)(unsafe.Pointer(&originalSlice[0]))
fmt.Printf("The address of the underlying array before modifySlice: %p\n", arrayPtr)

modifySlice(originalSlice)
fmt.Println("Original slice after modifySlice:", originalSlice)
arrayPtr = (*int)(unsafe.Pointer(&originalSlice[0]))
fmt.Printf("The address of the underlying array after modifySlice: %p\n", arrayPtr)

modifySlicePointer(&originalSlice)
fmt.Println("Original slice after modifySlicePointer:", originalSlice)
arrayPtr = (*int)(unsafe.Pointer(&originalSlice[0]))
fmt.Printf("The address of the underlying array after modifySlicePointer: %p\n", arrayPtr)
}

Run code In Go1.22

If originalSlice is:

1
2
originalSlice := make([]int, 3, 4)
originalSlice[0], originalSlice[1], originalSlice[2] = 0, 1, 2

The result will be:

1
2
3
4
5
6
7
8
9
10
Original slice before modifySlice: [0 1 2]
The address of the underlying array before modifySlice: 0xc000126000
Inside modifySlice (modified slice): [100 1 2 200]
The address of the underlying array in modifySlice: 0xc000126000
Original slice after modifySlice: [100 1 2]
The address of the underlying array after modifySlice: 0xc000126000
Inside modifySlicePointer (modified slice pointer): [100 1 2 200]
The address of the underlying array in modifySlicePointer: 0xc000126000
Original slice after modifySlicePointer: [100 1 2 200]
The address of the underlying array after modifySlicePointer: 0xc000126000

You may have a question about why Original slice after modifySlice is [100 1 2], not [100 1 2 200]. This is because its length is 3, so only three numbers are output.

If originalSlice is:

1
originalSlice := []int{0, 1, 2}

The result will be:

1
2
3
4
5
6
7
8
9
10
Original slice before modifySlice: [0 1 2]
The address of the underlying array before modifySlice: 0xc0000ac000
Inside modifySlice (modified slice): [100 1 2 200]
The address of the underlying array in modifySlice: 0xc0000b2030
Original slice after modifySlice: [100 1 2]
The address of the underlying array after modifySlice: 0xc0000ac000
Inside modifySlicePointer (modified slice pointer): [100 1 2 200]
The address of the underlying array in modifySlicePointer: 0xc0000b2060
Original slice after modifySlicePointer: [100 1 2 200]
The address of the underlying array after modifySlicePointer: 0xc0000b2060

The result will be more complex. You can run the code and think about it first.

This example has several points that need to be noticed:

  1. Why is 0xc0000b2030 not equal to 0xc0000ac000? This is because expansion occurred, causing the underlying array address to change. The main function’s result is [100 1 2].
  2. Why is 0xc0000b2060 not equal to 0xc0000ac000? This is also due to expansion.
  3. But why does the main function’s address remain 0xc0000b2060, and its output is [100 1 2 200]? This is because we passed a copy of the original slice’s address to the function, which changed the address to point to a new slice, so this effect is global.

7. Iterating over a slice using range

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
func main() {
u := []user{
{"wesley", "medium"},
{"tfrain", "github"},
}
n := make([]*user, 0, len(u))
for _, v := range u {
fmt.Println("%p\n", &v)
n = append(n, &v)
}
fmt.Println(n)
for _, v := range n {
fmt.Println(v)
}
}
// print before Go 1.22
// 0xc000060020
// 0xc000060020
// [0xc000060020 0xc000060020]
// &{tfrain github}
// &{tfrain github}

// print after Go 1.22
// 0xc000098020
// 0xc000098040
// [0xc000098020 0xc000098040]
// &{wesley medium}
// &{tfrain github}

In Go before 1.22, when using the range loop to iterate over a slice u, the address of variable v does not change, as shown in the example where it always remains 0xc000060020. Therefore, the output after copying is not intuitive. Of course, this unintuitive issue has been fixed in Go 1.22, see: Fixing For Loops in Go 1.22 - The Go Programming Language

8. Summary

If you want to master the basic usage of slices, just remember three points:

  1. Understand how to use len and cap
  2. Understand value copying in Go
  3. Know that a slice is backed by an array and has a strategy for resizing

9. References

Go Slices: usage and internals - The Go Programming Language


More Series Articles about You Should Know In Golang:

https://wesley-wei.medium.com/list/you-should-know-in-golang-e9491363cd9a

And I’m Wesley, delighted to share knowledge from the world of programming. 

Don’t forget to follow me for more informative content, or feel free to share this with others who may also find it beneficial. it would be a great help to me.

Give me some free applauds, highlights, or replies, and I’ll pay attention to those reactions, which will determine whether or not I continue to post this type of article.

See you in the next article. 👋

中文文章: https://programmerscareer.com/zh-cn/golang-slice/
Author: Wesley Wei – Twitter Wesley Wei – Medium
Note: Originally written at https://programmerscareer.com/golang-slice/ at 2024-07-21 19:58. If you choose to repost or use this article, please cite the original source.

Sustainability Reflections for My Website's Development My 10+ Obsidian Plugins As a Medium Writer

Comments

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×