Make your Go code efficient using make() when creating slices

Photo by Andrew Neel on Unsplash

Make your Go code efficient using make() when creating slices

Introduction

Slice is one of the powerful data types in Go which allows combining related values of the same type. In this blog, we will explore how we can improve the performance of your Go code faster using make() function when creating slices.

For better comparison, we initially create a slice using the var keyword.

Using var keyword:

In the below example, we create a very simple slice:

package main

func main() {
    usingVar()
}
func usingVar() []string {
    var sports []string
    sports = append(sports, "Volleyball")
    sports = append(sports, "Swimming")
    sports = append(sports, "Running")
    sports = append(sports, "Badminton")
    sports = append(sports, "Baseball")
    sports = append(sports, "Hockey")
    return sports
}

Here, a sports slice is created with an empty array, with length and capacity being zero and value being nil.

var sports []string // len - 0, cap - 0, value=nil

As you may know, during append Go runtime will create a new array if there is no space in the underlying array. Since there is no space in our sports slice, the below steps are performed by Go runtime when executing this code,

  1. When we append "Volleyball", the underlying array length is zero and hence Go runtime will create a new array with a length and capacity equal to one.

  2. When we append "Swimming", again there is no space in the array. Due to this, the below steps are executed:

    • Create a new array with a length and capacity of two

    • Go runtime will copy the value "Volleyball" from the existing array to the newly created array

    • Append "Swimming" to a newly created array

    • Delete the old array

  3. The same procedure is followed for the remaining appends as well.

You can imagine the total time and computing resources spent when there are millions of records.

By the way, the same story is also valid for slices which are initialized using := syntax like below,

sports := []string{}

Using make() function:

make() would be a better choice when you approximately know the size of the elements you are going to store. make() function takes the below parameters to create a slice,

  1. data type,

  2. length,

  3. capacity - Optional. When not provided, length is considered for capacity.

Here is the extension of the above code using make().

package main

func main() {
    usingVar()
    usingMake()
}
func usingVar() []string {
    var sports []string
    sports = append(sports, "Volleyball")
    sports = append(sports, "Swimming")
    sports = append(sports, "Running")
    sports = append(sports, "Badminton")
    sports = append(sports, "Baseball")
    sports = append(sports, "Hockey")
    return sports
}
func usingMake() []string {
    sports := make([]string, 0, 6)
    sports = append(sports, "Volleyball")
    sports = append(sports, "Swimming")
    sports = append(sports, "Running")
    sports = append(sports, "Badminton")
    sports = append(sports, "Baseball")
    sports = append(sports, "Hockey")
    return sports
}

usingMake(), we ask to create a slice using make() with zero as the length and capacity to six for the underlying array. This means the underlying array can store up to six elements.

sports := make([]string, 0, 6)

So in this example, there is no need to create new arrays as we have only six elements and the current array is capable of storing these six elements. Hence, we save time and computation power.

Even if we have to append an additional value(7th element) to the sports slice, Go runtime will create a new array with double the capacity of the underlying array - in this example, capacity would be increased to 12. Still, it's better to use make() as the new array is created only when the size grows beyond the consciously provided capacity.

So, use make() when creating slices to improve the performance of your code.

How to measure the performance?

We can use the "benchmark" tool in Go to measure the performance for usingVar() and usingMake() functions to know the efficiency gain.

package main

import (
    "testing"
)

func BenchmarkWithMake(b *testing.B) {
    for i := 0; i < b.N; i++ {
        usingMake()
    }
}

func BenchmarkWithVar(b *testing.B) {
    for i := 0; i < b.N; i++ {
        usingVar()
    }
}

With the above code, we have dedicated for loop to test the performance of usingMake() and usingVar() functions. During benchmark execution, b.N used in for loop will be adjusted at runtime until we get a reliable result.

Below command has to be executed to trigger the performance measurement.

go test -bench .

Here are the results:


BenchmarkWithMake-10            18519922                57.97 ns/op
BenchmarkWithVar-10              7669695               156.8 ns/op
PASS
ok      GoBlogs 2.767s
  • BenchmarkWithMake():

    • executed on 10 cores - represented by BenchmarkUsingMake-10

    • loop ran for 18519922 times - value for b.N, determined at runtime

    • took 57.97 ns per iteration on average.

  • BenchmarkWithVar():

    • executed on 10 cores - represented by BenchmarkWithVar-10

    • loop ran for 7669695 times - value for b.N, determined at runtime

    • took 156.8 ns per iteration on average.

This blog explains how to improve code performance by using the make() function to create slices instead of using var keyword or := syntax.

Benchmarking with the Go tool showed that make() was 2.7 times faster than var, and is recommended for efficient code.