Slices

An array has a fixed size. A slice, on the other hand, is a dynamically-sized, flexible view into the elements of an array. In practice, slices are much more common than arrays.

The type []T is a slice with elements of type T. A slice is formed by specifying two indices, a low and high bound, separated by a colon: a[low : high]. This selects a half-open range which includes the first element, but excludes the last one.

primes := [6]int{2, 3, 5, 7, 11, 13}
var s []int = primes[1:4] // creates slice from array
fmt.Println(s)
  • A slice does not store any data, it just describes a section of an underlying array.

  • Changing the elements of a slice modifies the corresponding elements of its underlying array.

  • Other slices that share the same underlying array will see those changes.

  • Slice operator could be applied to slices too.

  • Создание слайса выполняется за константное время.

  • Contrary to arrays, slices is not comparable. You can only compare slice to nil

Creating slice

Convert array to slice with a slice operator:

a := [5]int{1,2,4,5}
s1 := a[0:5]
s2 := a[:]

s1 and s2 are sharing the same undelying slice as a

When slicing, you may omit the high or low bounds to use their defaults instead. The default is zero for the low bound and the length of the slice for the high bound. These slice expressions are equivalent:

a[0:10]
a[:10]
a[0:]
a[:]

A slice literal is like an array literal without the length.

var s = []bool{true, true, false}

// Initialize only the 100th element with an empty string.
var s2 = []string{99: "magic"}

The make function allocates a zeroed array and returns a slice that refers to that array:

a := make([]int, 5)  // len(a)=5
b := make([]int, 0, 5) // len(b)=0, cap(b)=5

You can create a slice using an empty slice literal. This creates a zero-length slice, which is non-nil (comparing it to nil returns false). Otherwise, a nil slice works identically to a zero-length slice.

var x = []int{}

If you have a good idea of how large your slice needs to be, but don’t know what those values will be when you are writing the program, use make. The question then becomes whether you should specify a nonzero length in the call to make or specify a zero length and a nonzero capacity.

  • If you are using a slice as a buffer, then specify a nonzero length.

  • If you are sure you know the exact size you want, you can specify the length and index into the slice to set the values. This is often done when transforming values in one slice and storing them in a second.

  • In other situations, use make with a zero length and a specified capacity. This allows you to use append to add items to the slice. If the number of items turns out to be smaller, you won’t have an extraneous zero value at the end. If the num‐ ber of items is larger, your code will not panic.

Slice state

A slice has both a length and a capacity.

  • The length of a slice is the number of elements it contains.

  • The capacity of a slice is the number of elements in the underlying array, counting from the first element in the slice.

The length and capacity of a slice s can be obtained using the expressions len(s) and cap(s). You can extend a slice's length by re-slicing it, provided it has sufficient capacity.

s := []int{2, 3, 5, 7, 11, 13}
s = s[:0] // Slice the slice to give it zero length.
s = s[:4] // Extend its length.
s = s[2:] // Drop its first two values.

At any time the following relationship holds:

0 <= len(s) <= cap(s)

For slice[i:j] with an underlying array of capacity k

  • Length: j - i

  • Capacity: k - i

Third index in slice operator gives you control over the capacity of the new slice. The purpose is not to increase capacity, but to restrict the capacity. As you’ll see, being able to restrict the capacity of a new slice provides a level of protection to the underlying array and gives you more control over append operations.

s := []string{"Apple", "Orange", "Plum", "Banana", "Grape"}
s2 := source[2:3:4]

For slice[i:j:k] or [2:3:4]:

  • Length: j-i or 3-2=1

  • Capacity: k-i or 4-2=2

By having the option to set the capacity of a new slice to be the same as the length, you can force the first append operation to detach the new slice from the underlying array. Detaching the new slice from its original source array makes it safe to change.

Выполняя slice оператор над существующим слайсом выполняются следующие правила:

  • создание слайса s[i:j] где j > cap(s) приведет к ошибке т.к. в нижележащем массиве нет данных.

  • создание слайса s[i:j], где len(s) < j < cap(s) приведет к расширению слайса

nil slice (Empty slice)

A nil slice has a length and capacity of 0 and has no underlying array:

// Create a nil slice of integers.
var slice []int

if slice == nil // true

An empty slice has a length and capacity of 0 has underlying array of capacity 0:

// Use make to create an empty slice of integers.
slice := make([]int, 0)
  • An nil slice contains a nil reference to underlying array. They’re useful when you want to represent a slice that doesn’t exist, such as when an exception occurs in a function that returns a slice

  • An empty slice contains a zero-element underlying array that allocates no storage. Empty slices are useful when you want to represent an empty collection, such as when a database query returns zero results.

  • Because a nil slice doesn’t require any allocation, we should favor returning a nil slice instead of an empty slice.

  • A nil slice is marshaled as a null element, whereas a non-nil, empty slice is marshaled as an empty array. If we work in the context of strict JSON clients that differentiate between null and [], it’s essential to keep this distinction in mind.

Add elements to slice (append function)

// Create a slice of integers.
// Contains a length and capacity of 4 elements.
slice := []int{10, 20, 30, 40}
// Append a new value to the slice.
// Assign the value of 50 to the new element.
newSlice := append(slice, 50)

The first parameter s of append is a slice of type T, and the rest are T values to append to the slice. The resulting value of append is a slice containing all the elements of the original slice plus the provided values. If the backing array of s is too small to fit all the given values a bigger array will be allocated.

  • The returned slice will point to the newly allocated array.

  • It is a compile-time error if you forget to assign the value returned from append. Passing a slice to the append function actually passes a copy of the slice to the function. The function adds the values to the copy of the slice and returns the copy. You then assign the returned slice back to the variable in the calling function.

func append(s []T, vs ...T) []T

The append operation is clever when growing the capacity of the underlying array. Capacity is always doubled when the existing capacity of the slice is under 1,000 elements. Once the number of elements goes over 1,000, the capacity is grown by a factor of 1.25, or 25%. This growth algorithm may change in the language over time.

The append is also a variadic function. This means you can pass multiple values to be appended in a single slice call. If you use the ... operator, you can append all the elements of one slice into another.

Note that we need to accept a return value from append as we may get a new slice value.

// Create two slices each initialized with two integers.
s1 := []int{1, 2}
s2 := []int{3, 4}
s3 := append(s1, s2...);

Iterate over slices

Go has a special keyword called range that you use in conjunction with the keyword for to iterate over slices.

slice := []int{10, 20, 30, 40}
for index, value := range slice {
    ...
}

Passing slices between functions

Passing a slice between two functions requires nothing more than passing the slice by value. Since the size of a slice is small, it’s cheap to copy and pass between functions.

On a 64-bit architecture, a slice requires 24 bytes of memory. The pointer field requires 8 bytes, and the length and capacity fields require 8 bytes respectively. Since the data associated with a slice is contained in the underlying array, there are no problems passing a copy of a slice to any function. Only the slice is being copied, not the underlying array

Copying slices

The copy function takes two parameters. The first is the destination slice and the second is the source slice. It copies as many values as it can from source to destination, limited by whichever slice is smaller, and returns the number of elements copied. The capacity of x and y doesn’t matter; it’s the length that’s important.

d := make([]string, len(s), len(s))
copy(d, s)
fmt.Println("cpy:", d)

You can use copy with arrays by taking a slice of the array. You can make the array either the source or the destination of the copy.

x := []int{1, 2, 3, 4} 
d := [4]int{5, 6, 7, 8}
y := make([]int, 2)
copy(y, d[:])
fmt.Println(y)
copy(d[:], x)
fmt.Println(d)

Memory leaks

For the first case, leaking capacity. When we create sub-slice, the original slice still exists in the memory and referenced by subslice. In this case it just hang in the memory and do nothing.

func getMessageType(msg []byte) []byte {
    return msg[:5]
}

It’s essential to keep this rule in mind when working with slices: if the element is a pointer or a struct with pointer fields, the elements won’t be reclaimed by the GC.

When we use the slicing operation with pointers or structs with pointer fields, we need to know that the GC won’t reclaim these elements. In that case, the two options are to either perform a copy or explicitly mark the remaining elements or their fields to nil.

Multi-dimensional slices

twoD := make([][]int, 3)
for i := 0; i < 3; i++ {
   innerLen := i + 1
   twoD[i] = make([]int, innerLen)
   for j := 0; j < innerLen; j++ {
       twoD[i][j] = i + j
   }
}

Last updated