Tricks to save memory in Golang


Unless you’re prototyping a service, you probably care about your application’s memory usage. With a smaller memory footprint, the infrastructure costs are reduced, and scaling becomes a bit easier/delayed. Even though Go is known for not consuming a lot of memory, there are ways to reduce the consumption further. Some of them require much refactoring, but many are easy to do.

Pre-allocate your slices in Golang

To understand this optimization, we have to understand how slices work in Golang, and to do that, we have to understand arrays first.

There is a very good blog post on this topic on go.dev.

Arrays are a collection of the same type with continuous memory. An array type definition specifies a length and an element type.

The main issue with arrays is that they are fixed size – they cannot be resized, as the array’s length is part of their type.

Unlike array type, slice type doesn’t have a specified length. A slice is declared the same way as an array, without the element count.

Slices are wrappers for arrays, they do not own any data – they are references to arrays. They consist of a pointer to the array, the length of the segment, and its capacity (number of elements in the underlying array).

When you append to a slice that doesn’t have the capacity for a new value – a new array is created with a larger capacity and the values from the current array are copied to the new one. This leads to unnecessary allocations and CPU cycles.

To better understand this, let’s take a look at the following

Golang Code Snippet

func main() {
    var ints []int
    for i := 0; i < 5; i++ {
        ints = append(ints, i)
        fmt.Printf("Address: %p, Length: %d, Capacity: %d, Values: %v\n",
            ints, len(ints), cap(ints), ints)
    }
}

The above outputs:

Address: 0xc000018030, Length: 1, Capacity: 1, Values: [0]
Address: 0xc000018050, Length: 2, Capacity: 2, Values: [0 1]
Address: 0xc000082020, Length: 3, Capacity: 4, Values: [0 1 2]
Address: 0xc000082020, Length: 4, Capacity: 4, Values: [0 1 2 3]
Address: 0xc000084040, Length: 5, Capacity: 8, Values: [0 1 2 3 4]

Looking at the output, we can conclude that whenever capacity had to be increased (by a factor of 2), a new underlying array had to be created (new memory address) and values were copied to the new array.