Useful tips of slice tricks in golang

About Slices

In Go, arrays with a fixed length are called arrays, while those without a fixed length are called slices. Operations on slices typically include creation, reading, updating, appending, and deletion. Creation can utilize make and new; reading and updating are similar to other programming languages, where the some_array[index] method can be used; appending involves either adding elements at the end or inserting elements at specific positions. For appending, Go’s standard library has an append method available, but there are no corresponding functions for insertion and deletion; the closest, the delete method, can only be used with the map type (Go’s map type is like dictionaries or hashes in other languages). Hence, here are some techniques for operating slices in Go.

Making of slice

At the beginning, it was mentioned that slices can be created using make or new. Here are the corresponding syntax examples:

a := make([]T, 5)      // Declare a slice a with a length and capacity of 5
b := make([]T, 5, 10)  // Declare a slice b with a length of 5 and a capacity of 10
c := new([]T)          // Declare a pointer c to a slice

T represents a type (int, string, myStruct…). If appending elements to a slice exceeds its initially specified length or capacity, Go will automatically reallocate a larger memory block to this slice and copy the original content into the new memory block, a process similar to C’s realloc, only Go does it for you.

Getting the length and capacity of a slice

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

Prototype of the append method

func append(slice []Type, elems ...Type) []Type

Type represents a type (int, string, myStruct…). This function accepts two sets of parameters: the first set is the slice to which elements will be added, and the second set is variadic parameters.

Adding an element b to slice a

a = append(a, b)  // Equivalent to pushing an element b into slice a

Add three elements b, c, d to slice a

a = append(a, b, c, d)

Adding a slice b to slice a

a = append(a, b...)

Here, the is not an ellipsis but part of the syntax. Note that the slice b cannot take an array because, in Go, an array’s length is also a part of its type. Therefore, an int array of length 3 and an int array of length 5 are different types, not to mention a variable-length slice. The b… syntax only accepts slices! So, what if a programmer wants to add an array to the end of a slice? Just convert the array to a slice first:

// Create an array of length 3
array := [3]int{1, 2, 3}
// Create slice b from array
// For conversion between array and slice, refer to "Array and Slice Conversion" in this article
b := array[:]

// Append slice b to slice a
a = append(a, b...)

Prototype of the copy method

func copy(dst, src []Type) int

Type represents a type (int, string, myStruct…). This function takes two parameters: the first is the destination of the copy, and the second is the source of the copy, simply put, it copies from the latter to the former. The return value is the number of elements copied. Copy slice a to slice b

Copying slice a to slice b

// Create a slice b with the same length as slice a
b = make([]T, len(a))
// Copy slice a to slice b
copy(b, a)

T represents a type, please substitute it with int, string, myStruct… Think about it, isn’t copying just like adding everything into an empty collection? So, you could also do this:

b = append([]T(nil), a...)

This method showcases a little trick: []T(nil) is a type []T, a slice of length 0, and append will add all elements of slice a into this empty slice []T(nil), then return the reference to slice b. What if the sizes of the copies differ? Let’s discuss two scenarios:

a := []int{ 0, 1, 2 }  // a is a slice of length 3
b := []int{ 3, 4 }     // b is a slice of length 2
num := copy(a, b)      // Copy b into a, and store the number of copied elements into num

The result of executing this code is:

a = [3, 4, 2]
b = [3, 4]
num = 2

Let’s reverse the order of copying and see the result:

a := []int{ 0, 1, 2 }  // a is a slice of length 3
b := []int{ 3, 4 }     // b is a slice of length 2
num := copy(b, a)      // Copy a into b, and store the number of copied elements into num

The result of executing this code is:

a = [0, 1, 2]
b = [0, 1]
num = 2

From here, we can see that in Go, copying two collections of different sizes does not cause any issues; copying always goes by the smaller collection’s length.

Extracting a part of a slice into a new slice

Suppose slice a contains 6 elements as follows:

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

To extract [2, 3, 4] from slice a, you can do this:

a = append(a[2:5])

The syntax [i:j] means i <= {x} < j, where i and j are index values within the slice, and {x} represents the set of elements that meet the index condition. So, [2:5] refers to the elements a[2], a[3], a[4]. To extract [0, 1, 4, 5] from slice a, you can do this:

a = append(a[:2], a[4:]...)

The syntax [i:] means i <= {x}, where {x} includes all elements from index i to the end of the collection.

The syntax [:j] means {x} < j, where {x} includes all elements from the start of the collection up to the element before index j.

To extract [1, 3, 5] from slice a, you can do this:

a = append(a[:0], a[1], a[3], a[5]) // a[:0] is equivalent to an empty slice

If you only want 3 specific elements from slice a, the concept is similar to adding these 3 specific elements into an empty slice, hence the use of a[:0] trick here. Because the append function’s first set of parameters is a slice, and we want the first set of parameters to be an empty slice without declaring a new empty slice variable, we use the previously mentioned [:j] syntax to take elements from the start of the collection up to the element before index j. Since j here is 0, meaning no elements are taken, it results in an empty collection.

Deleting specific elements from a slice

Understanding the [i:j] syntax and its application, you can further use this method to delete specific elements from a slice.

Suppose slice a contains 6 elements as follows:

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

To remove element 3 from slice a, you can do this:

a = append(a[:3], a[4:]...)

To express it as a template without affecting the order, to remove the ith element from slice a:

a = append(a[:i], a[i+1:]...)

Expanding this template expression to remove all elements included in i <= {x} < j:

a = append(a[:i], a[j:]...)

Of course, if you’ve understood all the operations so far, you can also use this clever method to delete element 3:

a = a[:3+copy(a[3:], a[4:])]

This expression is equivalent to the following code:

num := copy(a[3:], a[4:])  // num = 2, a = [ 0, 1, 2, 4, 5, 5 ]
a = a[:3+num]

The expression copy(a[3:], a[4:]) can be further broken down into a[3:] and a[4:]. a[3:] represents [3, 4, 5] with 3 elements, while a[4:] represents [4, 5] with 2 elements. Following the principle mentioned in the copy section earlier, copying is done based on the smaller collection’s length, so this expression’s operation copied 2 elements, resulting in 2, and hence the original expression can be further simplified to:

a = a[:3+2]

Performing the operation again:

a = a[:5]

As mentioned before, [:j] means from the start of the collection up to the **(j-1)**th element, so a[:5] represents [0, 1, 2, 4, 5], successfully removing element 3. If the slice is just a collection to you and the order of elements inside doesn’t matter, there’s another way to remove element 3. First, let’s explain the parallel assignment method borrowed from interpreted languages in Go:

a, b = 3, 5

This expression is equivalent to in C language:

a = 3;
b = 5;

Parallel assignment is not just syntactic sugar for simplifying assignment operations. In C language, to swap two values, you need to declare another temporary variable to assist, for example:

// a = 3, b = 5 -> a = 5, b = 3
tmp = a;  // tmp = 3
a = b;    // a = 5
b = tmp;  // b = 3

But parallel assignment can directly swap:

a, b = 3, 5  // a = 3, b = 5
a, b = b, a  // a = 5, b = 3

Returning to the topic of removing element 3, here’s another clever method:

a[3], a = a[len(a)-1], a[:len(a)-1]

The effect of this line of expression is equivalent to:

// a = [ 0, 1, 2, 3, 4, 5 ]
a[3] = a[len(a)-1]  // a[3] = 5, a = [ 0, 1, 2, 5, 4, 5 ]
a = a[:len(a)-1]    // a = [ 0, 1, 2, 5, 4 ], Effectively popping an element out of slice a

The first expression is intuitive; len(a) represents the length of slice a, and a[len(a)-1] represents the last element of the slice. This expression means to store the last element 5 of slice a into a[3]. The second expression is also straightforward because [:j] means from the start of the collection up to the **(j-1)**th element, so this expression means to take up to the second-to-last element from the start of slice a, in other words, discarding the last element of slice a.

Analyzing this clever method, it’s just 2 steps:

  1. Replace the position value of the element to be deleted with the last element of the slice.
  2. Remove the duplicated last element.

When performing the [i:j] operation on a slice, discarding either the first or the last element only requires one index value, which is more convenient. Therefore, the above method can be varied by replacing the position value of the element to be deleted with the first element and then discarding the duplicated first element:

// a = [ 0, 1, 2, 3, 4, 5 ]
a[3] = a[0]  // a[3] = 0, a = [ 0, 1, 2, 0, 4, 5 ]
a = a[1:]    // a = [ 1, 2, 0, 4, 5 ]

Expressed as a template without maintaining order, to remove the ith element from slice a:

a[i], a = a[0], a[1:]

Doesn’t it look simpler now?

Garbage Collection and Potential Memory Leak Issues

Go has a garbage collection mechanism, so it automatically releases memory space that is no longer needed. However, since slices are reference types, if the memory address referenced by slice elements is still accessible, Go will consider this memory still needed and will not actively release it. For one-off, simple programs, this issue need not be considered, as Go will release the memory before exiting upon completion. However, for large programs, especially server-like programs that run uninterrupted, avoiding potential memory leaks becomes necessary.

Below are enhanced versions of some slice operations mentioned in the previous sections, all centered around setting the address of slice elements no longer needed to the zero value: Removing elements from slice a where i <= {x} < j:

copy(a[i:], a[j:])
for k, n := len(a)-j+i, len(a); k < n; k++ {
    a[k] = nil  // Or set to the zero value of slice a's type
}
a = a[:len(a)-j+i]

While maintaining order, delete the element at position i from slice a:

a[i] = nil  // Or set to the zero value of slice a's type
a = append(a[:i], a[i+1:]...)

Without maintaining order, delete the element at position i from slice a:

a[i], a[len(a)-1], a = a[len(a)-1], nil, a[:len(a)-1]  // Delete the last element
a[i], a[0], a = a[0], nil, a[1:]                       // Delete the last element

In Go, all variables that are not actively assigned are initialized to the zero value of their type upon creation. Here is a simple reference table:

Type Zero Value Type Zero Value Type Zero Value Type Zero Value
booleans false integers 0 floats 0.0 strings ""
pointers nil functions nil interfaces nil slices nil
slices nil channels nil maps nil

In Go, nil is equivalent to NULL in C. If the entire slice is no longer needed, the following method can be used to clear the entire slice’s reference, and Go will recycle the memory that is no longer needed:

// Declare a slice a
a := []int{1, 2, 3, 4, 5}
fmt.Println("a, len(a), cap(a) =", a, len(a), cap(a))
// Discard the entire slice a
a = nil
fmt.Println("a, len(a), cap(a) =", a, len(a), cap(a))

The result of executing this code is:

a, len(a), cap(a) = [1 2 3 4 5] 5 5
a, len(a), cap(a) = [] 0 0  // The slice has been removed

Manually expanding the length of a slice (adding to the end)

First, create a slice a using make:

a := make([]int, 5)

The content, length, and capacity of this slice a are:

a = [ 0, 0, 0, 0, 0 ]
len(a) = 5
cap(a) = 5

Then, expand the length of this slice a by 3, adding to the end of slice a:

a = append(a, make([]int, 3)...)

Breaking it down internally, make([]int, 3) means creating a slice of length 3. Using the syntax mentioned earlier, this newly created slice is added to slice a. Initially, the length and capacity of slice a are both 5, but after adding a slice of length 3 to slice a, the result becomes:

a  [ 0, 0, 0, 0, 0, 0, 0, 0 ]
len(a) = 8
cap(a) = 10

When Go detects that the length expansion exceeds the capacity, it automatically doubles the original capacity.

Manually expanding the length of a slice (inserting in the middle)

Similarly, the current content, length, and capacity of slice a are listed as:

a  [ 0, 1, 2, 3, 4 ]
len(a) = 5
cap(a) = 5

Expand the length of this slice a by 3, inserting at position 2 in slice a

a = append(a[:2], append(make([]int, 3), a[2:]...)...)

This expression and the following code are equivalent:

b := make([]int, 3)      // b = [ 0, 0, 0 ]
b = append(b, a[2:]...)  // b = [ 0, 0, 0, 2, 3, 4 ]
a = append(a[:2], b...)  // a = [ 0, 1, 0, 0, 0, 2, 3, 4 ]

These three lines represent:

  1. Create a slice b with a length of 3.
  2. Add all elements of slice a from position 2 onwards to the end of the newly created slice b.
  3. Add all elements of slice b to the collection of elements in slice a before position 1. The final result is:
a = [ 0, 1, 0, 0, 0, 2, 3, 4 ]
len(a) = 8
cap(a) = 10

Expressed as a template, at the ith position of slice a, insert a slice of length j:

a = append(a[:i], append(make([]int, j), a[i:]...)...)

Inserting slice b into slice a

Suppose slice a is an empty slice of length 5, and slice b is a non-empty slice of length 3:

a := make([]int, 5)		// a = [ 0, 1, 2, 3, 4 ]
b := []int{ 7, 8, 9 }	// b = [ 7, 8, 9 ]

Insert slice b at position 2:

a = append(a[:2], append(b, a[2:]...)...)

Breaking this expression down into equivalent code for analysis:

b = append(b, a[2:]...)  // b = [ 7, 8, 9, 2, 3, 4 ]
a = append(a[:2], b...)  // a = [ 0, 1, 7, 8, 9, 2, 3, 4 ]

Similarly, if slice b contains only one element, the same operation is inserting one element into slice a.

Inserting 5 at position 2 of slice a:

a = append(a[:2], append([]int{5}, a[2:]...)...)
// a = [ 0, 1, 5, 2, 3, 4 ]

The hidden memory leak issue in nested append

Before this section, it might be helpful to review the garbage collection subsection mentioned earlier. Taking the nested append of inserting 5 at position 2 of slice a as an example:

a = append(a[:2], append([]int{5}, a[2:]...)...)

Breaking down this expression into equivalent code for analysis:

b := append([]int{5}, a[2:]...)  // b = [ 5, 2, 3, 4 ]
a = append(a[:2], b...)          // a = [ 0, 1, 5, 2, 3, 4 ]

Here, there are actually two slices involved: an inner slice b and an outer slice a. The final result resides in slice a, while the intermediate, temporary value slice b is no longer needed. However, because it is still accessible, Go considers it as not releasable, especially since slice b is implicitly declared without a variable name, leaving us no opportunity to set it to nil! Here is a more recommended implementation for inserting 5 at position 2 of slice a:

a = append(a, 0)    // a = [ 0, 1, 2, 3, 4, 0 ]
copy(a[3:], a[2:])  // a = [ 0, 1, 2, 2, 3, 4 ]
a[2] = 5            // a = [ 0, 1, 5, 2, 3, 4 ]

These three lines represent:

  1. Increase the length of slice a by one.
  2. Through the effect of copying, create a duplicate slot at the position where the element is to be inserted.
  3. Set the value of the duplicate element at position 2 to the inserted value 5. Expressed as a template expression, to insert element x at the ith position of slice a:
a = append(a, 0)
copy(a[i+1:], a[i:])
a[i] = x

Conversion between arrays and slices

First, let’s discuss how to convert a slice into an array that holds all elements of the slice to be converted.

Declare a slice of length 10 and capacity 10, named slice:

// Declare a slice
slice := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}

Then declare an array with 10 int type elements named array. As mentioned before, arrays in Go have a fixed size, so the syntax is as follows:

// Declare a fixed-length array of 10
array = [10]int{0,1,2,3,4,5,6,7,8,9}

Or, you can use the […] syntactic sugar to let the compiler determine the size of the array at compile time:

array = [...]int{0,1,2,3,4,5,6,7,8,9}

Both of the above declaration methods result in an array of the same type, [10]int. It’s emphasized again that in Go, the size of an array is part of its type. The most intuitive way is to assign values to each element of the array:

// Assign values to each element of the array
for i := 0; i < len(array); i++ {
	array[i] = slice[i]
}

Since the variable array must explicitly specify its size when declared, the result of len(array) is considered constant. Another way takes a bit of a detour, as mentioned before, the conversion is about making the array contain all elements of the slice to be converted. Isn’t this the same concept as the copy() function mentioned in the previous section? But the prototype of this copy() function only accepts two slices, and the variable array is of array type, so here’s how to convert an array into a slice:

slice := array[:]

This can be done with the : operator. The syntax [i:j] extracts all elements that meet the condition i <= {x} < j, and the [:] syntax used here means to take all elements! Think about it: extracting all elements from an array and then storing them in a container that has not explicitly specified a size, isn’t that a slice? After learning how to convert an array into a slice, you can then use the following method to convert a slice into an array:

// Copy all elements of the slice into the array
copy(array[:], slice[:])

Furthur Reading