skip to content
Alvin Lucillo

Go append slices

/ 3 min read

💻 Tech

Go append allows you to expand the length of your slice dynamically. This is because a slice has an underlying capacity that defines that maximum elements that can be allocated to the underlying/backing array. This is because a slice is composed of the pointer to the backing/underlying array, length of the elements, and the capacity of the backing array. Once the length and capacity are equal, append extends the backing array by double its size. At runtime, you can only access up to the length of the slice, while the remaining unallocated spaces are reserved for future appends.

Note that append uses the concept of value data semantics. This means append operates on its own copy of the slice. Once you assign the value of the append to the original slice, the pointer to the old backing array is unused, and the garbage collector can now free up those unused spaces. This is illustrated by the code below:

data = append(data, data1)

The good thing about appending to slices is that the capacity is already calculated for us. In the code below, we can see that the capacity is doubled (100%) then only extended to just 25% of the original capacity at the latter part of the loop. If we let the append extend that for us with zero slices from the beginning, there will be some extra processing. See the code below.

But what if from the beginning we already set the capacity with make? See the second example at the bottom.

Demo 1:

func main() {
	var numbers []int

	lastCapacity := cap(numbers)

	fmt.Println("length | current capacity | change in capacity ")

	for i := 0; i < 1e5; i++ { // 1e5 is 100000 (1 followed by 5 zeroes)
		numbers = append(numbers, i)
		if lastCapacity != cap(numbers) {
			fmt.Printf("%6v | %16v | %2.2f%%\n", i, cap(numbers), float64(cap(numbers)-lastCapacity)/float64(lastCapacity)*100)
			lastCapacity = cap(numbers)
		}
	}
}

Demo 1 output:

length | current capacity | change in capacity 
     0 |                1 | +Inf%
     1 |                2 | 100.00%
     2 |                4 | 100.00%
     4 |                8 | 100.00%
     8 |               16 | 100.00%
    16 |               32 | 100.00%
    32 |               64 | 100.00%
    64 |              128 | 100.00%
   128 |              256 | 100.00%
   256 |              512 | 100.00%
   512 |              848 | 65.62%
   848 |             1280 | 50.94%
  1280 |             1792 | 40.00%
  1792 |             2560 | 42.86%
  2560 |             3408 | 33.12%
  3408 |             5120 | 50.23%
  5120 |             7168 | 40.00%
  7168 |             9216 | 28.57%
  9216 |            12288 | 33.33%
 12288 |            16384 | 33.33%
 16384 |            21504 | 31.25%
 21504 |            27648 | 28.57%
 27648 |            34816 | 25.93%
 34816 |            44032 | 26.47%
 44032 |            55296 | 25.58%
 55296 |            69632 | 25.93%
 69632 |            88064 | 26.47%
 88064 |           110592 | 25.58%

With the capacity already set in the beginning with make, there’s no change to the capacity happening, that’s why there’s no output except for the heading. This demonstrates that if we know the size of the collection from the beginning, it will be more efficient when we set the capacity. If we don’t know the capacity from the get-go, then we will make use of the resizing capability of append.

Demo 2:

func main() {
   // var numbers []int
   numbers := make([]int, 0, 1e5)

   lastCapacity := cap(numbers)

   fmt.Println("length | current capacity | change in capacity ")

   for i := 0; i < 1e5; i++ {
   	numbers = append(numbers, i)
   	if lastCapacity != cap(numbers) {
   		fmt.Printf("%6v | %16v | %2.2f%%\n", i, cap(numbers), float64(cap(numbers)-lastCapacity)/float64(lastCapacity)*100)
   		lastCapacity = cap(numbers)
   	}
   }
}

Demo 2 output:

length | current capacity | change in capacity