Looks like some new Gophers are confused about the difference between var and make().

First of all, let’s talk about Go’s approach to types and values. Go doesn’t have a null value like some other languages. This means a variable will always have some value, even when you define it without an assignment. Well, how does Go handle these defined variables, you ask? Simple, Go assigns the “zero value” of that type to the variable. For example, when you write var x int, even if you don’t assign anything, the value of x will be 0.

But hang on. I said there is no null value in Go, but there is something called nil. What is it, you may ask? nil is the zero value for slices, maps, pointers, channels, and interfaces. Pointers and interfaces are a bit special, but for slices, maps, and channels, you often need a way to initialize them with a non-zero, usable state before you start throwing data at them.

This is where the make() function comes in. make() initializes the underlying data structures for these specific types, so they are ready to be used, and it returns a value of that type.

Slices

Let’s say you want to create a slice and fill it with data. If you only define it var s []int, you get a nil slice. Well, Go’s append() function works fine on a nil slice, since append() also can create a new underlying array if the current one is full, but if you try to insert data at a specific index like s[0] = 5, it will panic because there is no underlying array yet.

When you call make([]int, 0), Go allocates an array and creates a slice header pointing to it. Now you have allocated space in memory. More importantly, you can manage the size of the array (which is capacity of the slice) while calling make(). This makes sure Go won’t have to constantly create new arrays and copy old data over as you add new items. For example, let’s say you will dynamically add data to the slice, but you know the length won’t exceed 10. You can write make([]int, 0, 10). This creates the underlying array with a size of 10 right at the beginning, saving you from unnecessary allocations.

Channels

nil is also the zero value for channels. If you try to read from or write to a nil channel, it will block forever. To make a channel usable, it needs to be initialized, and it needs to know if it is buffered or unbuffered.

To define this, we again use the make() function. Let’s say you have a channel and you don’t want a goroutine to write to it unless there is another goroutine ready to read that data. You can use an unbuffered channel for this by writing make(chan int).

Or, let’s say you don’t want to block writes until there are 10 unread data points sitting in the channel. For that, you need a buffered channel, which you can create by writing make(chan int, 10).

Maps

Maps are also reference types that point to underlying data structures holding key-value pairs. Like slices, a nil map doesn’t have anywhere to put new data. While you can safely read from a nil map (it will return the zero value of the value type of the map), trying to write to a nil map will cause a panic.

The make() function simply allocates and initializes the memory required for the hash map structure so it can safely store your data. You can even pass a size hint to it, like make(map[string]int, 100), to pre-allocate space if you know roughly how many keys you’ll be storing.

In short, make() is exclusively designed for slices, maps, and channels to initialize their internal data structures. In order to use the variable without assigning from the begining, you need the make() function.