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 than a (but it might be used as typed nil slice for append built-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:

  1. Any of slicing operator argument is negative value (e.g. s[neg:], but also s[:len(empty)-2])!
  2. If the following expression is not true: low <= high && high <= max (e.g. s[2:1]).
  3. 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 clear also 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:

  • -1 means that the left slice is considered lesser than the right slice;
  • 0 means that both slices are considered equal (inclusive the same length);
  • and 1 means 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.Values for just simple forward iteration over slice values;
  • slices.Chunk for 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]):

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.Sort in-situ sorting into ascending order, but available only for slices of comparable base type;
  • slices.SortFunc in-situ sorting with customer comparator function;
  • slices.Sorted producing new slice sorted in ascending order, but available only for slices of comparable base type;
  • slices.SortStableFunc stable sorting algorithm variant of slice.Sort with custom comparator function;
  • slices.SortedStableFunc stable sorting algorithm variant of slice.Sorted with custom comparator function;

One can use predicate to check if the given slice is sorted or not:

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:

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):

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.

Grow always 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:

  1. Be aware of the difference between nil slice and empty slice!
  2. No need to distinguish, always use len to predicate slice emptiness!
  3. 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:

  1. Direct modification to slice is shared for all slices sharing the same underlying array!
  2. 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/.


<
Previous Post
AWS CloudWatch vol. 1
>
Next Post
Go caveats vol. 1