Go slices
- Introduction to arrays
- Slice representation
- Initializing slices
- Slicing operator
- Built-ins for operating on slices
- Generic functions operating on slices
- Slice caveats
Introduction to arrays
It is important to understand arrays in Go before deep dive into slices. Array is homogenous linear data
structure of static size. Using other words, array is a continuous block of memory that consists of elements
of the same data type. Number of element kept in an array together with element data type forms the type of
array. Therefore, [3]int is different type than [4]int even though they both share the same base type
(int).
Due to Go’s value semantics, be careful about using bigger arrays. Every array value assignment does truly make a copy of whole array. Mind that hidden copy of array is done when ranging over array, as one might see on the following code snippet:
type dataArray = [10_000_000]uint64
func dumbPrintOfArrayCopy(a dataArray) { // one copy
fmt.Println("[")
for i, x := range a { // yet another one copy
fmt.Printf(" %5d: %d,\n", i, x)
}
fmt.Println("]")
}
As comments in the previous snippet mentions, that not-so-small array is passed by value to the function
(the first copy of an array is made) and when starting for-range loop its expression is evaluated
(effectively meaning that the second copy of an array might be made). This highly depends on the Go compiler
version (as new optimizations are being introduced with almost each minor release).
Usage of array might bring some benefits. To name a few:
- two arrays of the same base type can be compared for value equality using just
==operator, but only if its base type is comparable; - arrays in struct can be kept together with other struct’s data, so locality principle might bring performance gain;
- smaller arrays does not escape on heap so often (compiler has space for some extra heuristics);
- assignment makes a new copy (might be both benefit or disadvantage, depending on use case).
If one need to initialize sparse array in a static way, then advanced syntax of array literal might be useful.
The following code snippet creates an array with 100 elements with just 10th and the very last element
initialized to 1, but all other elements initialized to zero value: array := [...]int{9: 1, 99: 1}
(the type of that array is [100]int), as one might see on the following example:
func Test_array_SparseArrayLiteral(t *testing.T) {
array := [10]byte{0, 1, 2, 0, 0, 0, 0, 0, 0, 3}
sparseArray := [...]byte{1: 1, 2, 9: 3}
assert.Equal(t, array, sparseArray)
}
Slice representation
Using generic type T, slice type is []T. Concrete example of slice types are, e.g. []byte, []bool,
[]int, but also []struct{x int; y int} (slice of anonymous structs), or [][]int (matrix, aka slice of
slices). Mind that if there is integer (e.g. [4] meaning array of four elements) or ellipsis ([...]
to infer size of an array based on the given array literal) between the brackets, it is array type.
In contrast, slice type does not have anything in between brackets.
Slice is very similar to array, but its size can vary throughout the time. Therefore, slice can grow, but it can also shrink. Slice introduces property called capacity to support growth effectively. Slice still have length, and it is homogenous linear collection, so all its element are of the same base type.
Every slice is represented by so-called fat pointer, being a pointer with some extra data fields included. Every slice is represented by 3 fields. Namely, pointer to the first element of slice, current length of slice, and current capacity of slice. The complete fat pointer might be held on a stack, so query for length of slice does not need extra dereference, so all the basic operations with slice are quite fast.
|-----|-----|-----|-----|
| 1 | 2 | 3 |/////|
|-----|-----|-----|-----|
^
| |---|
|---|-× | pointer (to the first element)
|---|
| 3 | length (count of elements currently occupying slice)
|---|
| 4 | capacity (available size of underlying array)
|---|
Multiple slices might share the same underlying array, so space requirements might be kept low. Slices are designed to be “dynamically sized array”, so they might grow as needed while underlying array is re-allocated or even replaced under the hood.
A special variant of slice is so-called nil slice. This is slice, that does not have any underlying array. It is still kept as a fat pointer in the memory, so we might visualize it as:
|-----|
| nil | nil pointer (no underlying array being referenced)
|-----|
| 0 | zero length
|-----|
| 0 | zero capacity
|-----|
Initializing slices
There are several ways how to initialize a slice. Let us look on the overview code snippet at first:
var a []int
b := ([]int)(nil)
c := []int{} // obsolete
d := make([]int, 0)
e := make([]int, 0, 20)
f := []int{1, 2, 3} // [1 2 3]
g := []int{4: 0} // [0 0 0 0 1]
h := []int{1, 0, 3: 3, 4} // [1 0 0 3 4]
Let us take a look on each of presented slices:
a— preferable way of constructing the nil slice;b— nil slice, but less readable thana(but it might be used as typed nil slice forappendbuilt-in function);c— literal of nil slice, but linters dislike it, so it is better to not use it at all;d— empty slice with underlying array, so it is different to nil slice;e— empty slice with pre-allocated underlying array for 20 elements (len(e) == 0 && cap(e) == 20);f— non-empty slice literal with all elements explicitly stated using position;g— non-empty slice literal with all zero elements and size denoted by index-value pair;h— non-empty slice literal with more complex semantics.
Slicing operator
Slicing operator is used to create slice out of array or slice. As simple as slice := array[low:high] it is,
where low is index of the first element included in the slice and high is index of the first element excluded.
The length (aka count of elements) of the resulting slice is given by high - low formula (e.g. array[0:1]
is a slice of just one element). The capacity of slice is by default calculated to be able to use full capacity
of the underlying array. The following illustration presents two slices on the same underlying array
(created by expressions array[1:2] and array[0:1]):
|---|
|---|-× | pointer
| |---|
| | 1 | length
| |---|
| | 3 | capacity
| |---|
v
|-----|-----|-----|-----|
| 1 | 2 | 3 | 4 |
|-----|-----|-----|-----|
^
| |---|
|---|-× | pointer
|---|
| 1 | length
|---|
| 4 | capacity
|---|
One can override default capacity while using full slicing operator using the syntax
slice := array[low:high:max]. The desired capacity is set based on max - low expression.
This is important to ensure that copy of underlying array is done on any append to the slice
(not to affect slice trailing elements of the original underlying array).
So more safe is to use always array[1:2:2], so the slice will have both length and capacity of 1.
The following unit test should provide information about basic usage of slicing operator:
func Test_SlicingOperator(t *testing.T) {
original := []byte{1, 1, 2, 3, 5, 8, 13, 21}
assert.Equal(t, 8, len(original))
assert.Equal(t, 8, cap(original))
// suffix of the last two elements (meaning s[6:8])
suffixSlice := original[len(original)-2:]
assert.Equal(t, []byte{13, 21}, suffixSlice)
assert.Equal(t, 2, len(suffixSlice))
assert.Equal(t, 2, cap(suffixSlice))
// prefix of the first two elements (meaning s[0:2])
prefixSlice := original[:2]
assert.Equal(t, []byte{1, 1}, prefixSlice)
assert.Equal(t, 2, len(prefixSlice))
assert.Equal(t, cap(original), cap(prefixSlice))
// all except the first and the last element (meaning s[1:7])
subSlice := original[1 : len(original)-1]
assert.Equal(t, []byte{1, 2, 3, 5, 8, 13}, subSlice)
assert.Equal(t, len(original)-2, len(subSlice))
assert.Equal(t, cap(original)-1, cap(subSlice))
// all (meaning s[0:8])
completeSlice := original[:]
assert.Equal(t, original, completeSlice)
assert.Equal(t, 8, len(completeSlice))
// prefix with explicit capacity higher than length !!!
prefixSlice = original[:3:5]
assert.Equal(t, []byte{1, 1, 2}, prefixSlice)
assert.Equal(t, 3, len(prefixSlice))
assert.Equal(t, 5, cap(prefixSlice))
// prefix with explicit capacity same as length
prefixSlice = original[:3:3]
assert.Equal(t, []byte{1, 1, 2}, prefixSlice)
assert.Equal(t, 3, len(prefixSlice))
assert.Equal(t, 3, cap(prefixSlice))
}
Idiomatic usage of slicing operator is to reset buffer length to 0 (buf = buf[:0]), re-using the
underlying array without need of do new allocation and putting extra pressure on garbage collector.
Be careful as slicing operator panic for any invalid input:
- Any of slicing operator argument is negative value (e.g.
s[neg:], but alsos[:len(empty)-2])! - If the following expression is not true:
low <= high && high <= max(e.g.s[2:1]). - If any of slicing operator argument address element outside the original slice (e.g.
s[:len(s)+1]).
Built-ins for operating on slices
Comparing slices
Slice supports == and != operators, but only for pointer-wise equality check.
Meaning, it can be used just for comparison of the given slice against nil.
If you need to compare two slices content-wise (having the same length and holding the same values),
use slices.Equal function of standard library.
func TestSliceEquality(t *testing.T) {
var a []int
assert.Nil(t, a, "nil slice")
assert.True(t, a == nil, "nil slice using `==`")
b := make([]int, 0)
assert.NotNil(t, b, "empty but not nil slice")
assert.True(t, b != nil, "empty but not nil slice using `!=`")
assert.True(t, slices.Equal(a, b), "nil slice and empty slice are equal")
assert.True(t, slices.Equal([]int{1}, []int{1}), "slices.Equal")
}
Checking length and capacity — len and cap
To query the current length of slice, use len built-in function.
Simply calling len(x) for slice held by variable x will return single value of int type
(simply returning te value stored within fat pointer representing that slice).
It is safe to call len function even on not yet initialized slice variable as it correctly returns 0.
This function supports not just slices, but also arrays and maps.
It is worth to note that nil slice and empty slice cannot be distinguished using len function.
If your code does not need to handle those two differently, keep using always len function,
and ignore that difference.
Fat pointer of each slice variable also holds capacity of the slice.
Use cap built-in function to retrieve the capacity
of slice held by given variable. To find out a capacity of slice x, call simply cap(x) returning
an int value. Also cap can be called on nil slice (aka not yet initialized slice variables) returning 0.
func TestSliceLengthAndCapacity(t *testing.T) {
var a []int
assert.Equal(t, 0, len(a), "length of empty slice")
assert.Equal(t, 0, cap(a), "capacity of empty slice")
b := []int{1, 2, 3}
assert.Equal(t, 3, len(b), "length of slice")
assert.Equal(t, 3, cap(b), "capacity of slice")
}
Appending elements to slice — append
The most often used built-in function for slice handling is definitely
append. It accepts a slice argument and variable
amount of elements. All passed elements are append to the slice and new slice is returned (one shall
not forget to assign the resulting slice). If there is enough capacity, the original underlying array
is used. Otherwise, new underlying array with higher capacity is allocated, original data is copied,
and extra elements are append to the fresh new slice returned to the caller.
func testAppend(t *testing.T, a []int, length int, capacity int, newElemCount int) []int {
t.Helper()
a = append(a, make([]int, newElemCount)...)
assert.Equal(t, length, len(a), "length")
assert.Equal(t, capacity, cap(a), "capacity")
return a
}
func TestSlice_append_doubling(t *testing.T) {
var a []int
a = testAppend(t, a, 0, 0, 0)
a = testAppend(t, a, 1, 1, 1)
a = testAppend(t, a, 2, 2, 1)
a = testAppend(t, a, 3, 4, 1)
a = testAppend(t, a, 4, 4, 1)
a = testAppend(t, a, 5, 8, 1)
a = testAppend(t, a, 7, 8, 2)
a = testAppend(t, a, 9, 16, 2)
}
The current version of Go v1.24, is using increment step of 100 % (effectively doubling it) up to size of 1024 elements. After reaching that increment step is reduced to just 50 % of the current capacity. The general rule is: the bigger current capacity is, the smaller portion of current capacity is used to as increment step.
Mind that algorithm for re-allocation of underlying array might be adjusted by any of future Go releases.
func TestSlice_append_min_size(t *testing.T) {
testAppend(t, make([]int, 8), 9, 16, 1)
testAppend(t, ([]int)(nil), 1000, 1024, 1000)
testAppend(t, make([]int, 1024), 1025, 1024+512, 1)
testAppend(t, make([]int, 1024+512), 1024+512+1, 1024+512+768, 1)
}
Go idiom is using append to concatenate two slices. Any slice might be passed as so-called “vararg” not
just to append built-in function. Just be careful, if the first slice has enough capacity to hold both
merged slices, the first and merged will share the same underlying array. The idea or merging two slices
of the same base type is captured by the following example:
func Test_MergeTwoSlices(t *testing.T) {
first := []byte{12, 24, 36}
second := []byte{9, 11, 16}
merged := append(first, second...)
assert.Equal(t, []byte{12, 24, 36, 9, 11, 16}, merged)
}
Copying data between slices — copy
For copying of data between two slices is designed copy built-in function.
Its usage is following: copy(destSlice, srcSlice). Number of copied slices is based on formula
min(len(destSlice), len(srcSlice)) and that number is also returned. By default, both slices
should have the same base type, but there is a special case allowing to copy bytes from a string
to a slice of bytes.
Zeroing all elements — clear
Since Go v1.21 was introduced clear built-in function.
If you call clear(slice) then slice will have each of its elements set to zero value of its type.
func Test_clear_slice(t *testing.T) {
s := []int{1, 2, 3}
assert.Equal(t, 3, len(s))
assert.Equal(t, 1, s[0])
clear(s)
assert.Equal(t, 3, len(s))
assert.Zero(t, s[0])
assert.True(t, slices.Equal([]int{0, 0, 0}, s))
}
One can use
clearalso on map, but it has shifted semantics — all elements from the given map are removed completely, so map will become empty (aka with length of zero).
Generic functions operating on slices
Some time ago, Go offered experimental slices package introducing a generic functions implementing
excessively used algorithms. The current version of Go (v1.24) already offers a plethora of functions
moved into standard slices package. Let us take a look on its
current offerings.
Equality and ordering
Often, one has to check if length and contained elements of two slices are equal (not being the same pointer).
Calling slices.Equal does the job, but it might be used just for
slices of comparable base type (e.g. struct containing map cannot be compared using ==).
For custom equal predicate or incomparable base type slices is meant to be used
slices.EqualFunc, which awaits two slices of the same base type
and predicate function used to decide whether two elements are equal or not.
func Test_slices_equal(t *testing.T) {
assert.True(t, slices.Equal([]int{1, 2}, []int{1, 2}))
assert.False(t, slices.Equal([]int{1, 2}, []int{1, 10_240}))
assert.False(t, slices.Equal([]int{1, 2}, []int{1, 2, 42}))
eqMod10 := func(l int, r int) bool { return l%10 == r%10 }
assert.True(t, slices.EqualFunc([]int{1, 2}, []int{11, 102}, eqMod10))
}
For modeling of comparison based on ordered types (aka those implementing cmp.Ordered interface)
can be used functions:
slices.Compare or
slices.CompareFunc.
The first is using cmp.Compare function on each pair of slice elements. The second expects you to
provide custom comparator function returning:
-1means that the left slice is considered lesser than the right slice;0means that both slices are considered equal (inclusive the same length);- and
1means that the left slice is considered greater than the right slice.
func TestSlice_slices_compare(t *testing.T) {
assert.Equal(t, 0, slices.Compare([]int{1, 2}, []int{1, 2}))
assert.Equal(t, -1, slices.Compare([]int{1, 2}, []int{1, 10_240}))
assert.Equal(t, -1, slices.Compare([]int{1, 2}, []int{1, 2, 42}))
cmpMod10 := func(l int, r int) int { return cmp.Compare(l%10, r%10) }
assert.Equal(t, 0, slices.CompareFunc([]int{1, 2}, []int{11, 102}, cmpMod10))
}
Iterators
We have two iterators producing just one value (based on iter.Seq[E]):
slices.Valuesfor just simple forward iteration over slice values;slices.Chunkfor getting of chunks (sub-slices) of the given size (effective enabling batch processing of potentially large slices).
And there are another two iterators producing two values, namely index and value (based on iter.Seq2[int, E]):
slices.Allfor forward iteration;slices.Backwardfor backward (reversed) iteration.
Sorting and searching
Of course, one can use the original approach of sort package.
So usage of sort.Ints(sliceOfInts) or sort.Slice(slice, fnCustomIsLess) is still feasible solution.
For sorting of slice can be also used the following generic functions:
slices.Sortin-situ sorting into ascending order, but available only for slices of comparable base type;slices.SortFuncin-situ sorting with customer comparator function;slices.Sortedproducing new slice sorted in ascending order, but available only for slices of comparable base type;slices.SortStableFuncstable sorting algorithm variant ofslice.Sortwith custom comparator function;slices.SortedStableFuncstable sorting algorithm variant ofslice.Sortedwith custom comparator function;
One can use predicate to check if the given slice is sorted or not:
slices.IsSortedfor slices of comparable base type (only ascending order);slices.IsSortedFuncwith customer comparator function.
func TestSlice_slices_isSorted(t *testing.T) {
sorted := []int{1, 2, 3}
random := []int{3, 1, 2}
assert.True(t, slices.IsSorted(sorted))
assert.False(t, slices.IsSorted(random))
}
We can also search in slice already sorted in ascending order. Both following functions are returning index (of found element or where to insert the given element) and found flag:
BinarySearchfor slices of comparable base type;BinarySearchFuncwith customer equal predicate.
func TestSlice_slices_search(t *testing.T) {
slice := []int{1, 2, 4, 8}
// searching for existing element
elementIndex, found := slices.BinarySearch(slice, 4)
assert.True(t, found)
assert.Equal(t, 2, elementIndex)
// searching for place where to insert missing element
insertIndex, notFound := slices.BinarySearch(slice, 3)
assert.False(t, notFound)
assert.Equal(t, 2, insertIndex)
}
If we have already sorted slice, we can compute a new deduplicated slice (replacing single copy for each consecutive run):
Compactfor slices of comparable base type;CompactFuncwith custom equal predicate.
func TestSlice_slices_compact(t *testing.T) {
assert.Equal(t, []int{1, 2, 3}, slices.Compact([]int{1, 2, 2, 3, 3, 3}))
}
Slice capacity management
slices.Grow accepts numeric value n of type int and
ensures that there is enough space for next n appended elements without re-allocation of the underlying
array.
Growalways panics for negative n, but it might also panic for value of n that is too large to allocate the memory.
slices.Clip is useful for removing unused capacity from the slice.
Other utility functions
For shallow copy might be used slices.Clone (using copying elements
using simple assignment).
slices.Contains is predicate reporting whether the given value
exists in the given slice. For incomparable base type or any more complex predicate required for existence check
might be used slices.ContainsFunc.
While slices.Index, reps. a more general sibling
slices.IndexFunc, returns an index of the leftmost element
found, reps. fulfilling the given predicate function, or -1.
For reversing all elements of the given slice in place can be used
slices.Reverse. Reversal is done in situ.
slices.Insert insert one or more values at the given index of already
existing slice. It panics if the given index is out of slice (meaning not in range 0 <= index <= len(slice)).
This function takes care of all the following steps: re-allocation, copy of original content and shifting trailing
elements. New slice is returned, so mind to assign it correctly.
slices.Replace accepts a slice (to be modified), two indices
(denoting range of indices — sub-slice — to be replaced), and varargs (element to be used a replacement).
It panics in case of indexing issues.
Any previously used trailing elements (in case of smaller replacement) are replaced by zeroes.
slices.Delete accepts a slice (to be modified) and two indices
(denoting range to be deleted). It panics in case of indexing issues. Any previously used trailing elements
are replaced by zeroes.
It has similar name, but its purpose differs a lot. We are talking about generic function of
slices.DeleteFunc. This one is more of a filter
function known from functional paradigm. It accepts an original slice and predicate function producing
a brand-new slice where are copied only those elements for which predicate evaluated to true.
slices.Concat take all the given slices and return brand-new slice
created by concatenating all of them.
slices.AppendSeq combine the given slice by appending all
the elements of the given iterator (of type iter.Seq[E]) into the returned extended slice.
slices.Repeat return a new slice created by repeating the given
slice the given number of times. Result is never nil. It panics on negative count or overflow of calculated
size for a new slice.
For searching of extremes of slice values might be used slices.Min,
slices.MinFunc, slices.Max,
and slices.MaxFunc. Beware, that all of those functions panic for
empty slice. Also for floating-point types the first NaN will be propagated to the output.
Perhaps, I overlooked some, but hopefully all the important functions were already mentioned.
Slice caveats
Differencing nil slice and empty slice
One has to be aware that Go code might distinct between nil slice and initialized empty slice. Calling
len and cap built-in functions on both are simply returning 0, so they behave the same from their
point of view. Nevertheless, comparison against nil is where the difference comes in.
func Test_differenceBetweenNilSliceAndEmptySlice(t *testing.T) {
var (
nilSlice []bool
emptySlice = make([]bool, 0)
)
assert.True(t, len(nilSlice) == 0 && cap(nilSlice) == 0, "similarity I")
assert.True(t, len(emptySlice) == 0 && cap(emptySlice) == 0, "similarity II")
assert.True(t, nilSlice == nil, "difference I")
assert.True(t, emptySlice != nil, "difference II")
}
To be honest, the majority of real-world use cases do not need to distinguish between nil slice and empty slice. This is the preferred way.
On the other side, something that common as JSON marshalling is where it matters.
func Test_JsonMarshalNilSliceAndEmptySlice(t *testing.T) {
type T struct {
XS []bool `json:"xs"`
}
nilSlice, _ := json.Marshal(T{([]bool)(nil)})
emptySlice, _ := json.Marshal(T{make([]bool, 0)})
assert.Equal(t, []byte(`{"xs":null}`), nilSlice)
assert.Equal(t, []byte(`{"xs":[]}`), emptySlice)
}
Summary:
- Be aware of the difference between nil slice and empty slice!
- No need to distinguish, always use
lento predicate slice emptiness! - Marshaling slice into JSON is one example where it matters!
Modification of shared underlying array
Sharing of underlying array until next re-allocation is another information one has to be aware of.
At first, we initialize one slice using s1 := make([]int, 3, 4). That slice has underlying array
that consist of 3 integers (all the elements of zero value) and has capacity for up to four elements.
Then we use slicing operator s2 := s1[0:1] to initialize another slice using the same underlying
array. This status is captured by the following illustration:
|---|
|---|-× | pointer
| |---|
| | 3 | length
| |---|
| | 4 | capacity
| |---|
v
|-----|-----|-----|-----|
| 0 | 0 | 0 |/////|
|-----|-----|-----|-----|
^
| |---|
|---|-× | pointer
|---|
| 1 | length
|---|
| 4 | capacity
|---|
Then we append value of three to the slice using s2 = append(s2, 3), so value of both slices is updated.
It is questionable if this was done intentionally or not. The status is following:
|---|
|---|-× | pointer
| |---|
| | 3 | length
| |---|
| | 4 | capacity
| |---|
v
|-----|-----|-----|-----|
| 0 | 3 | 0 |/////|
|-----|-----|-----|-----|
^
| |---|
|---|-× | pointer
|---|
| 2 | length
|---|
| 4 | capacity
|---|
Yet another modification (but this time much more intuitive) can be obtained by simple assignment into
element that is already shared by both slices, e.g. s1[0] = 9. The status will be as shown here:
|---|
|---|-× | pointer
| |---|
| | 3 | length
| |---|
| | 4 | capacity
| |---|
v
|-----|-----|-----|-----|
| 9 | 3 | 0 |/////|
|-----|-----|-----|-----|
^
| |---|
|---|-× | pointer
|---|
| 2 | length
|---|
| 4 | capacity
|---|
The following unit test captures states from all three previous illustrations.
func Test_SharedUnderlyingArray(t *testing.T) {
s1 := make([]int, 3, 4)
s2 := s1[0:1]
assert.Equal(t, []int{0, 0, 0}, s1)
assert.Equal(t, []int{0}, s2)
assert.Equal(t, []int{0}, s3)
s2 = append(s2, 3)
assert.Equal(t, []int{0, 3, 0}, s1)
assert.Equal(t, []int{0, 3}, s2)
s1[0] = 9
assert.Equal(t, []int{9, 3, 0}, s1)
assert.Equal(t, []int{9, 3}, s2)
// EXTRA ROUND: full slicing operator is more safe
s3 := s1[0:1:1]
assert.Equal(t, []int{9}, s3)
s3 = append(s3, 42)
assert.Equal(t, []int{9, 42}, s3)
assert.Equal(t, []int{9, 3, 0}, s1)
assert.Equal(t, []int{9, 3}, s2)
}
Summary:
- Direct modification to slice is shared for all slices sharing the same underlying array!
- Appending to slice sharing underlying array is safe only for slice created by full slicing operator!
Inability to release unused memory
Imagine slice holding 10 MiB of data accepted over network. Using slicing operator to cut first 10 bytes
will not allow Go runtime to release any of those original 10 MiB of data. At least until your application
will keep at least one slice referencing that underlying array. Garbage collector cannot release just part
of underlying array. Perhaps, it will be solved by some later release of Go. Just be aware of this,
and use e.g. slices.Clone to make a copy of small sub-slice to be kept for longer time.
The same is true for buf = buf[:0] expression, that will keep the underlying array, so it will be re-used
as soon as the next input will be buffered (using e.g. buf = append(buf, msg...)). But this is somehow
intentional.
Built-in copy does not allocate
Using copy with non-initialized slice is mistake, one can do really easily.
func Test_UselessCopyVol1(t *testing.T) {
src := []int{1, 1, 1, 1}
var dest []int
copy(dest, src)
assert.NotEqual(t, src, dest)
assert.Zero(t, len(dest))
assert.Zero(t, cap(dest))
assert.Equal(t, 4, len(src))
assert.Equal(t, 1, src[3])
}
One should be aware, that capacity of slice is ignored. Amount of elements copied is limited by length of both source slice and destination slice. The shorter of those two slice lengths is used to determine count of copied elements.
func Test_UselessCopyVol2(t *testing.T) {
src := []int{1, 1, 1, 1}
dest := make([]int, 0, len(src))
copy(dest, src)
assert.NotEqual(t, src, dest)
assert.Zero(t, len(dest))
assert.Equal(t, 4, cap(dest))
assert.Equal(t, 4, len(src))
assert.Equal(t, 1, src[3])
}
One might also hit idiomatic code using append to do both initialization and copying operations on a single
line of code, like those on the following code snippet. Feel free to benchmark those different approaches yourself.
firstCopy := append(([]int)(nil), source...)
secondCopy := append(make([]int, 0, len(source)), source...)
Built-in copy function can be used, e.g. for step-by-step filling of some buffer.
The following example is artificial, but the basic idea is still the same:
func Test_EndiannessSwapUsingCopy(t *testing.T) {
bigEndian := []byte{0xD, 0xC, 0xB, 0xA}
middleEndianPDP11 := []byte{3: 0}
src := bigEndian
dest := middleEndianPDP11[2:] // limit to 2 elements
copy(dest, src)
src = bigEndian[2:] // limit to 2 elements
dest = middleEndianPDP11[:] // complete slice
copy(dest, src)
assert.Equal(t, []byte{0xB, 0xA, 0xD, 0xC}, middleEndianPDP11)
}
References
[1] T. Harsanyi, 100 Go Mistakes and How to Avoid Them. 2022. ISBN 978-1-61729-959-9.
[2] Google, Reference documentation for Go’s standard library [online]. Available on: https://pkg.go.dev/.
[3] ueokande, Go Slice Tricks Cheat Sheet [online]. Available on: https://ueokande.github.io/go-slice-tricks/.