Let’s Go with Go Slices
In Go programming, go slices are a major component to be known to develop applications. This article is to feed knowledge with some useful behaviors of “go slices”.
This article will focus on the main areas as follows about Go slices.
- Arrays
- Slices
- The capacity of a slice
- Internals
- Built-in functions helpful to slice operations
- Loop over a slice
- Equality of two slices
- Common Mistakes With Slices
If you need more understanding about slices in Go, you need to know what is called a slice. Go blog explains it as below.
The slice type is an abstraction built on top of Go’s array type, and so to understand slices we must first understand arrays. https://blog.golang.org/slices-intro
So, as a start, let’s get some idea about Arrays in Go.
Arrays
There are no many differences in Go arrays with other programming languages. We can declare an array by specifying the length and type of the elements. Arrays do not need to be initialized before use because all elements defaulted to their zero values.
The length of an array is a part of its type.
The array is not a reference type in go like slices, maps, channels, etc… Arrays are value types and the variable it is assigned holds the entire array, not a pointer to its first element like in C.
Slices
Because arrays are a bit inflexible, they are not famous in Go codes. Slices are the gift from Go to get that flexibility of an array. Slices are built on an array and we will discuss more them in the later internal section.
Slices have three main properties: type of elements, length of the slice, and the slice's capacity. The length and capacity of a slice can be taken by built-in len
and cap
functions.
We can create slices in the following ways
1. Using slice literal just like an array declaration
The default value of a slice is nil with zero length and zero capacity. Slice can be initialized with any number of elements with slice literal and length or capacity do not need to be mentioned. compiler counts it and saves with it slice type and it can be changed when adding or removing elements from the slice.
2. Using built-in make function
Slices can be created by calling the make function. Needs to be specified the slice type, length, and capacity (optional) to the slice. Capacity will have defaulted to its length if it is not specified. And capacity should be equal or greater than the length if it is specified in the make call.
Elements default to its’ zero values when creating a slice with make calls.
3. From slicing an existing slice or array.
Slicing means get a part(slice) from something. Like that slice can be created by re-slicing a slice. But both shared the same underline array.
And if there is an array already, we can create a slice from it by mentioning what part should be included.
The capacity of a slice
In above example “From slicing an existing slice or array”
, Length and capacity have different values. The length has expected value, but capacity? How does it return that value?
If the previous slice’s underlying array’s length equals l
and re-slice a slice with [i:j]
where i, j < l
, then new slice length equals to j-i
and capacity equals to l-i
.
In Go slices, capacity
is the number of elements in the underlying array counting from the start element of the slice.
Internals
When reading from the beginning of this article, Slice internals is mostly covered. So here is a summarization of them.
- The default value of the slice type is
nil
, Capacity and length are zero. - Slice has an underlying array. The array is creating when the slice is created from slice literal or built-in make function called.
- Slice has reference to the array element which is the starting element of the slice and number of elements.
- The length of a slice is the number of elements it has and the capacity of the slice is the number of elements in the underlying array counting from starting element of the slice.
Built-in functions helpful to slice operations
- copy
func copy(dst, src []T) int
copy function do copy slice to another. It makes a copy of the underlying array also. So both slices refer to different underlying arrays. This means if we change something in the previous slice, the copied slice will not be affected by it.
copy function has two parameters dst
(destination slice, where to copy) and src
( source slice, where copy from). Both source and destination should be the same type. And it returns the number of elements copied. Which will be the minimum of len(src)
and len(dst)
.
One of the best practices to use copy is when you passing a part of a large slice to a different function for further processes, copy that part to a new slice and pass that copied slice, because otherwise, that large slice’s underline array will remain in the heap. When you create a new slice with a small portion and share it with other functions, that large array can be garbage collected and free up the heap.
2. append
func append(slice []T, elements ...T) []T
append function appends the same type of elements to the sent slice and returns appended new slice. One or more elements can be appended at once. And also two slices can be appended to one with ...
parameter syntax.
If the underlying array’s capacity exceeds when do append, a new array will be created with double capacity and return appended slice on it.
One of the best practices to use append is when you know the length of the new slice you are appending, make that slice with the capacity of that length and append or create the new slice with that length and assign elements while iterating it. Because otherwise, that process creates multiple underline arrays up to the capacity of the new array after appending. This unnecessary allocation can be caused to a loss of performance.
3. sort.Slice
func Slice(x interface{}, less func(i, j int) bool)
In the sort standard package in Go, there is an implementation to sort slices with quick sort
. The slice to be sorted is the first parameter to the function and less func
is a callback function where the comparison of slice elements i, j
should be implemented accordingly how to slice should be sorted.
len
, cap
and make
are also built-in helpful functions to do slice operations. But they are already discussed above.
Loop over a slice
There are two ways to loop over a slice in Go. Normal incrementing for loop and accessing the index with iterating value and range looping.
If we use for i := range slice
for range loop, i
is the index of elements. If we use for i, v := range slice
for ranging, i
is the index and v
is the value of the element. Ranging ranged from beginning to the end of the slice.
Equality of two slices
As explained in Comparison Operators in go doc, slices can not be compared with ==
operator. So slices compared in go? There is a function in reflect
package called reflect.DeepEqual
.
Slice values are deeply equal when all of the following are true: they are both nil or both non-nil, they have the same length, and either they point to the same initial entry of the same underlying array (that is, &x[0] == &y[0]) or their corresponding elements (up to length) are deeply equal. Note that a non-nil empty slice and a nil slice (for example, []byte{} and []byte(nil)) are not deeply equal.
If there are two byte slices to compare, then bytes
package provides two functions bytes.Equal
and bytes.Compare
.
Apart from that, Custom comparison functions can be implemented to compare two slices if needed.
Arrays can be compared with ==
operator or reflect.DeepEqual
can be used.
Common Mistakes With Slices
Mistakes can be happened in anywhere when doing anything. It is common to the programming and as well as using Go slices. In my article about Common mistakes with go slices, I have discussed some mistakes I personally experienced in my programming life with go.