Go has a special make function that can be used to initialize channels, slices, and maps.

Using make, we can specify the memory and capacity constraints of the data type being created, giving us low-level control that’s not available to us using regular constructor functions

banner

Basic Usage

make is a special function in Go that can take a different number of types and arguments.

It returns an instance of the type passed as the first argument:

obj := make(someType, optionalArgument1, optionalArgument2)

Here, someType can be a slice, a map, or a channel.

Creating Slices

We can initialize slices of any type using make:

words := make([]string, 2)

Here, the first argument is the type and the second argument is the length.

By default, a new slice is initialized and filled with as many empty values as the length specified.

So, in this case, the value of words would be []string{"", ""}

We can also pass a third argument when creating a slice, which is the capacity. The capacity denotes how much memory is allocated to a slice, even though its length may not be as much.

For example, if we create words using capacity as well:

words := make([]string, 2, 5)

the value of words now is still []string{"", ""}, but the underlying memory is allocated for 5 string values.

representation of a slice with 2 memory slots assigned and 5 empty provisioned slots

So, if we add another element using append, Go doesn’t allocate any more memory under the hood:

words = append(words, "lorem ipsum")

By default, if you don’t assign any capacity, Go assumes a default capacity. When appending more items, Go provisions more capacity as and when needed.

The capacity argument has to be greater than the length argument, otherwise the code will not build

So, specifying the capacity is very useful if you know how big your slice will be beforehand, since we can skip the extra allocation each time the default capacity is exceeded.

Note that specifying the capacity doesn’t cap the max limit, but rather provisions the initial capacity that needs to be re-allocated when more elements are added

Creating Maps

Using make with maps is not as straightforward as with slices.

// make an empty map
m := make(map[int]string)

// make a map with optional capacity for `n` elements
m := make(map[int]string, n)

We can still make empty map instances, and specify the capacity, but the capacity here is taken by the language as a hint, and doesn’t make any guarantees about the exact capacity allocated.

The language specification itself mentions that the second argument describes “initial space for approximately n elements”

Creating Channels

We can create different types of channels using make:

  1. Unbuffered channels, that cannot store any data, and only act as a data pipe:
    // create an unbuffered channel of integers
    out := make(chan int)
    
  2. Buffered channels which can store some quantity of data
    // create a buffered channel, that can hold a maximum of 3 integer values
    out := make(chan int, 3)
    

Make vs New

Go also has a built in function called new, which often appears in similar scenarios as make, but has different functionality.

While make is able to allocate variable memory and returns an instance of the provided type, new can only initialize empty instances, and returns a pointer of the provided argument.

Let’s take a look at an example:

// returns a slice with 1 default (0) int element 
s1 := make([]int, 1)
// returns a pointer to an empty slice
s2 := new([]int)

fmt.Println(s1)
fmt.Println(s2)

If we run this code, we will get:

[0]
&[]

Conclusion

make is a versatile built-in function that can be used to initialize different data types.

The arguments and their significance depends on the type of variable being initialized.

You can read more about the details of how make and new work in the language specification.