iTranslated by AI

The content below is an AI-generated translation. This is an experimental feature, and may contain errors. View original article
💻

Arrays and Slices in Go

に公開

Looking at blog posts elsewhere, I think the things people often trip over when they start learning Go are interfaces, nil, and slices. Regarding Interface and nil, please refer to my previous articles. I realized I hadn't written about arrays and slices on Zenn yet, so I decided to write this article. How calculated of me (lol).

That said, slices are not that difficult once you understand their relationship with arrays. Let's look at them one by one below. Note that the diagrams in this article are borrowed from "Go Slices: usage and internals". To be honest, if you are comfortable with English, it might be faster to just read that article.

Array

First, let's talk about arrays.

An "array" in Go is a type of composite type, consisting of a sequence of data of a single type. In code, it looks like this[1].

sample1.go
// +build run

package main

import "fmt"

func main() {
    ary := [4]int{1, 2, 3, 4}
    fmt.Printf("Type: %[1]T , Value: %[1]v\n", ary)
    // Output:
    // Type: [4]int , Value: [1 2 3 4]
}

If we represent the variable ary in a diagram, it looks like this:


via “Go Slices: usage and internals - The Go Blog

The key point is that the type name is [4]int, which is fixed-length data. If the type or the number of elements in the array is different, they are treated as different types.

Also, an array is a "value". In other words, if they are of the same type, you can evaluate equality[2] using the == operator (different types cannot be evaluated against each other. Also, if the array type is not comparable, it cannot be evaluated).

sample2.go
func main() {
    ary1 := [4]int{1, 2, 3, 4}
    ary2 := [4]int{1, 2, 3, 4}
    ary3 := [4]int{2, 3, 4, 5}
    ary4 := [4]int64{1, 2, 3, 4}

    fmt.Printf("ary1 == ary2: %v\n", ary1 == ary2) // ary1 == ary2: true
    fmt.Printf("ary1 == ary3: %v\n", ary1 == ary3) // ary1 == ary3: false
    fmt.Printf("ary1 == ary4: %v\n", ary1 == ary4) // invalid operation: ary1 == ary4 (mismatched types [4]int and [4]int64)
}

Furthermore, because an array is a "value", a copy of the instance, including its contents, occurs in assignment syntax[3] such as =. Similarly, when an array is specified as a function argument, a copy is passed.

sample3a.go
func displayArray4Int(ary [4]int) {
    fmt.Printf("Pointer: %p , Value: %v\n", &ary, ary)
}

func main() {
    ary1 := [4]int{1, 2, 3, 4}
    ary2 := ary1

    fmt.Printf("Pointer: %p , Value: %v\n", &ary1, ary1)
    fmt.Printf("Pointer: %p , Value: %v\n", &ary2, ary2)
    displayArray4Int(ary1)
    // Output:
    // Pointer: 0xc0000141a0 , Value: [1 2 3 4]
    // Pointer: 0xc0000141c0 , Value: [1 2 3 4]
    // Pointer: 0xc000014240 , Value: [1 2 3 4]
}

If you want to pass the instance itself to a function, you can pass its pointer value.

sample3b.go
func referArray4Int(ary *[4]int) {
    fmt.Printf("Pointer: %p , Value: %v\n", ary, ary)
}

func main() {
    ary1 := [4]int{1, 2, 3, 4}

    fmt.Printf("Pointer: %p , Value: %v\n", &ary1, ary1)
    referArray4Int(&ary1)
    // Output:
    // Pointer: 0xc0000141a0 , Value: [1 2 3 4]
    // Pointer: 0xc0000141a0 , Value: &[1 2 3 4]
}

Is everything OK so far?

Slice

A slice in code looks like this[4].

sample4.go
func main() {
    slc1 := []byte{0, 1, 2, 3, 4}
    fmt.Printf("Type: %[1]T , Value: %[1]v\n", slc1)
    // Output:
    // Type: []uint8 , Value: [0 1 2 3 4]
}

The syntax difference from an array is whether you specify the number of elements inside the square brackets, but slices allow you to handle (seemingly) variable-length data sequences.

To create an empty slice, you can write it like this:

var slc1 []byte         // ZERO value
slc2 := []byte{}        // empty slice (size 0)
slc3 := make([]byte, 5) // empty slice (size 5)

Please note that accessing slc1[0] on a zero-value (nil) or size-0 slice will cause a panic.

Arrays can be converted to slices. Like this:

sample5.go
func main() {
    ary1 := [5]byte{0, 1, 2, 3, 4}
    slc1 := ary1[:]
    fmt.Printf("Pointer: %p , Refer: %p , Value: %v\n", &ary1, &ary1[0], ary1)
    fmt.Printf("Pointer: %p , Refer: %p , Value: %v\n", &slc1, &slc1[0], slc1)
    // Output:
    // Pointer: 0xc000012088 , Refer: 0xc000012088 , Value: [0 1 2 3 4]
    // Pointer: 0xc000004078 , Refer: 0xc000012088 , Value: [0 1 2 3 4]
}

Please notice the difference in pointer values for &x and &x[0] for the variables ary1 and slc1. It's natural for the pointer value of the variables to be different since they are different variables, but the pointers for each element of data are the same. In other words, the contents of the slice are the "same" as the assigned array.

In fact, the reality of a slice is an object that has three states as attributes:

  • A pointer value to the array it references
  • The size (can be obtained with the len() function)
  • The capacity (can be obtained with the cap() function)

In a diagram, it looks like this:


via “Go Slices: usage and internals - The Go Blog

Here

slc1 := ary1[:]

can be illustrated as follows:


via “Go Slices: usage and internals - The Go Blog

Using a slice, you can extract a portion of an array (or slice). For example,

slc2 := ary1[2:4]

would result in


via “Go Slices: usage and internals - The Go Blog

being extracted (note that the original array is not truncated). Furthermore, if you do

slc3 := sl2[:cap(slc2)]

for this slc2, it can be retrieved as


via “Go Slices: usage and internals - The Go Blog

like this.

sample6.go
func main() {
    ary1 := [5]byte{0, 1, 2, 3, 4}
    slc1 := ary1[:]
    slc2 := ary1[2:4]
    slc3 := slc2[:cap(slc2)]
    fmt.Printf("Refer: %p , Len: %d , Cap: %d , Value: %v\n", &ary1[0], len(ary1), cap(ary1), ary1)
    fmt.Printf("Refer: %p , Len: %d , Cap: %d , Value: %v\n", &slc1[0], len(slc1), cap(slc1), slc1)
    fmt.Printf("Refer: %p , Len: %d , Cap: %d , Value: %v\n", &slc2[0], len(slc2), cap(slc2), slc2)
    fmt.Printf("Refer: %p , Len: %d , Cap: %d , Value: %v\n", &slc3[0], len(slc3), cap(slc3), slc3)
    // Output:
    // Refer: 0xc000012088 , Len: 5 , Cap: 5 , Value: [0 1 2 3 4]
    // Refer: 0xc000012088 , Len: 5 , Cap: 5 , Value: [0 1 2 3 4]
    // Refer: 0xc00001208a , Len: 2 , Cap: 3 , Value: [2 3]
    // Refer: 0xc00001208a , Len: 3 , Cap: 3 , Value: [2 3 4]
}

Note that when using ary[low:high],

0 \le \mathrm{low} \le \mathrm{high} \le \mathrm{len(ary)}

must hold true. Also, if \mathrm{low} = 0 or \mathrm{high} = \mathrm{len(ary)}, you can omit the specification for \mathrm{low} or \mathrm{high}. In other words,

slc1 := ary1[:]

is equivalent to

slc1 := ary1[0:len(ary1)]

Alternatively, you can also write it as slc[low:high:max] to include the capacity specification.
In this case, \mathrm{max} specifies the capacity and is fine as long as it satisfies

0 \le \mathrm{low} \le \mathrm{high} \le \mathrm{max} \le \mathrm{cap(slc)}

Slices are references and values

As you can see from the explanation so far, slices behave like "references" to an array. Let's look a little more closely at what "behave" means.

sample7.go
func displaySliceByte(slc []byte) {
    fmt.Printf("Pointer: %p , Refer: %p , Value: %v\\n", &slc, &slc[0], slc)
}

func main() {
    ary1 := [5]byte{0, 1, 2, 3, 4}
    slc1 := ary1[:]
    fmt.Printf("Pointer: %p , Refer: %p , Value: %v\\n", &ary1, &ary1[0], ary1)
    fmt.Printf("Pointer: %p , Refer: %p , Value: %v\\n", &slc1, &slc1[0], slc1)
    displaySliceByte(slc1)
    // Output:
    // Pointer: 0xc000102058 , Refer: 0xc000102058 , Value: [0 1 2 3 4]
    // Pointer: 0xc000100048 , Refer: 0xc000102058 , Value: [0 1 2 3 4]
    // Pointer: 0xc000100078 , Refer: 0xc000102058 , Value: [0 1 2 3 4]
}

First, note that all three arrays/slices point to the same array. Also, notice that the slice passed as an argument to the displaySliceByte() function and the slice before passing it are different instances (meaning it's passed by value).

In this way, a slice only "behaves like a reference to an array" and is not a "reference" in the true sense of the word (as in Java, etc.).

People coming from languages where "references" like Java are built into the language specification might get confused here. The point that "there are no (true) references in Go" should be etched into your heart[5].

This gap between references and values is most clearly seen in the append() function[6].

sampe8.go
func main() {
    var slc []int
    fmt.Printf("Pointer: %p , <ZERO value>\\n", &slc)
    for i := 0; i < 5; i++ {
        slc = append(slc, i)
        fmt.Printf("Pointer: %p , Refer: %p , Value: %v (%d)\\n", &slc, &slc[0], slc, cap(slc))
    }
    // Output:
    // Pointer: 0xc000004078 , <ZERO value>
    // Pointer: 0xc000004078 , Refer: 0xc000012088 , Value: [0] (1)
    // Pointer: 0xc000004078 , Refer: 0xc0000120d0 , Value: [0 1] (2)
    // Pointer: 0xc000004078 , Refer: 0xc0000141c0 , Value: [0 1 2] (4)
    // Pointer: 0xc000004078 , Refer: 0xc0000141c0 , Value: [0 1 2 3] (4)
    // Pointer: 0xc000004078 , Refer: 0xc00000e340 , Value: [0 1 2 3 4] (8)
}

The append() function is a built-in function that adds data to the slice passed as an argument, but since the slc passed as an argument is just a "value," it returns the state of <pointer value, size, capacity> after the function execution as a slice instance. Meanwhile, the caller of the append() function overwrites the original slice's state with the return value.

Slices can neither be cloned nor compared

Since an array is a value, it is basically comparable, and a copy is created upon assignment. However, with slices, even if you use an assignment syntax like =, the contents are not cloned. If you need to clone a slice, use the copy() function.

sampe9.go
func main() {
    slc1 := []int{0, 1, 2, 3, 4}
    slc2 := slc1
    slc3 := make([]int, len(slc1), cap(slc1))
    copy(slc3, slc1)
    fmt.Printf("Pointer: %p , Refer: %p , Value: %v\n", &slc1, &slc1[0], slc1)
    fmt.Printf("Pointer: %p , Refer: %p , Value: %v\n", &slc2, &slc2[0], slc2)
    fmt.Printf("Pointer: %p , Refer: %p , Value: %v\n", &slc3, &slc3[0], slc3)
    // Output:
    // Pointer: 0xc000004078 , Refer: 0xc00000c2a0 , Value: [0 1 2 3 4]
    // Pointer: 0xc000004090 , Refer: 0xc00000c2a0 , Value: [0 1 2 3 4]
    // Pointer: 0xc0000040a8 , Refer: 0xc00000c2d0 , Value: [0 1 2 3 4]
}

This is natural because "assigning" a slice only copies the state of <pointer value, size, capacity>. Also, when using the copy() function, you must match the size and capacity of the destination instance in advance.

Furthermore, slices cannot be compared using the == operator, even between the same types (it will result in a compilation error. However, comparison with nil is possible).

sample10a.go
func main() {
    slc1 := []int{0, 1, 2, 3, 4}
    slc2 := []int{0, 1, 2, 3, 4}
    fmt.Printf("slc1 == slc2: %v\n", slc1 == slc2) // invalid operation: slc1 == slc2 (slice can only be compared to nil)
}

If you want to compare the contents of slices of the same type, you can use the reflect.DeepEqual() function, for example.

sample10b.go
func main() {
    slc1 := []int{0, 1, 2, 3, 4}
    slc2 := []int{0, 1, 2, 3, 4}
    if reflect.DeepEqual(slc1, slc2) {
        fmt.Println("slc1 == slc2: true")
    } else {
        fmt.Println("slc1 == slc2: false")
    }
    // Output
    // slc1 == slc2: true
}

Using the slices standard package [Added 2023-08-10]

Since Go 1.21, the slices standard package has been added. This defines slice operations using Generics. For example, methods for cloning or comparing slices are defined as follows:

// Clone returns a copy of the slice.
// The elements are copied using assignment, so this is a shallow clone.
func Clone[S ~[]E, E any](s S) S {
    // Preserve nil in case it matters.
    if s == nil {
        return nil
    }
    return append(S([]E{}), s...)
}
// Equal reports whether two slices are equal: the same length and all
// elements equal. If the lengths are different, Equal returns false.
// Otherwise, the elements are compared in increasing index order, and the
// comparison stops at the first unequal pair.
// Floating point NaNs are not considered equal.
func Equal[S ~[]E, E comparable](s1, s2 S) bool {
    if len(s1) != len(s2) {
        return false
    }
    for i := range s1 {
        if s1[i] != s2[i] {
            return false
        }
    }
    return true
}

Using these, the code from the previous section can be rewritten as follows:

sampe9b.go
package main

import (
    "fmt"
    "slices"
)

func main() {
    slc1 := []int{0, 1, 2, 3, 4}
    slc2 := slc1
    slc3 := slices.Clone(slc1)
    fmt.Printf("Pointer: %p , Refer: %p , Value: %v\n", &slc1, &slc1[0], slc1)
    fmt.Printf("Pointer: %p , Refer: %p , Value: %v\n", &slc2, &slc2[0], slc2)
    fmt.Printf("Pointer: %p , Refer: %p , Value: %v\n", &slc3, &slc3[0], slc3)
    // Output:
    // Pointer: 0xc000010018 , Refer: 0xc000072000 , Value: [0 1 2 3 4]
    // Pointer: 0xc000010030 , Refer: 0xc000072000 , Value: [0 1 2 3 4]
    // Pointer: 0xc000010048 , Refer: 0xc000072030 , Value: [0 1 2 3 4]
}
sample10c.go
package main

import (
    "fmt"
    "slices"
)

func main() {
    slc1 := []int{0, 1, 2, 3, 4}
    slc2 := []int{0, 1, 2, 3, 4}
    if slices.Equal(slc1, slc2) {
        fmt.Println("slc1 == slc2: true")
    } else {
        fmt.Println("slc1 == slc2: false")
    }
    // Output
    // slc1 == slc2: true
}

There are other useful methods as well, so be sure to check them out.

Conclusion

If you keep the relationship between arrays and slices in mind and use them appropriately, you should be able to handle them easily and safely (compared to arrays in C/C++, etc.). I hope you experiment with various patterns.

References

https://text.baldanders.info/golang/array-and-slice/
https://slide.baldanders.info/shimane-go-2020-02-13/

脚注
  1. When enumerating all elements of an array in a literal expression, you can omit the number of elements like ary := [...]int{1, 2, 3, 4}. Note that in this case, it is declared and initialized as an array, not a slice. As an application of this, there is also a way to specify only the last element, such as ary1 := [...]int{3: 4}. In this case, since the elements other than the last one are filled with zero values, it is equivalent to ary := [4]int{0, 0, 0, 4}. ↩︎

  2. I don't want to get involved in religious wars over the terms "equivalence" versus "equality" for operators, so I intentionally refer to "equality" as "dou-chi-sei" (identity/equality) in Japanese. Sorry about that. ↩︎

  3. In Go, assignment functions as a statement, not an expression. The difference between an expression and a statement is that a statement does not have an evaluation result as a value and cannot be incorporated as part of an expression. ↩︎

  4. The byte type is an alias for the uint8 type. ↩︎

  5. Other types that "behave like references" in Go include channels, interfaces, functions, and maps. These types, including slices, have a zero value of nil. ↩︎

  6. If you want to create a slice with a specified capacity, you can do something like slc := make([]int, 0, 5). Whether the instance (re)created by the make() or append() function resides on the stack or the heap depends entirely on optimization. ↩︎

GitHubで編集を提案

Discussion