iTranslated by AI
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].
// +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).
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.
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.
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].
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:
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.
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],
must hold true. Also, if
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,
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.
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].
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.
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).
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.
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:
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]
}
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
-
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 asary1 := [...]int{3: 4}. In this case, since the elements other than the last one are filled with zero values, it is equivalent toary := [4]int{0, 0, 0, 4}. ↩︎ -
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. ↩︎
-
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. ↩︎
-
The byte type is an alias for the uint8 type. ↩︎
-
Other types that "behave like references" in Go include channels, interfaces, functions, and maps. These types, including slices, have a zero value of nil. ↩︎
-
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 themake()orappend()function resides on the stack or the heap depends entirely on optimization. ↩︎
Discussion