Back to Blog

var vs make in Go: When to Use Each (with Examples)

var gives you the zero value, and make initializes slices, maps, and channels. When to use each in Go, and why writing to a nil map panics while appending to a nil slice does not.

var vs make in Go: When to Use Each (with Examples)

Go has two ways to create a variable, var and make, and picking the wrong one causes a lot of early bugs. The classic is a map that panics on its first write. A close second is a channel that blocks forever. This post shows when to use each one and why slices, maps, and channels are the special cases. Every example is runnable.

Table of Contents

What is the difference between var and make in Go?

var declares a variable and sets it to the type's zero value. It works for every type in the language. make only accepts a slice, map, or channel, the three built-in types that need runtime setup, and returns a value that can hold data right away. The zero value of those three is nil, so var leaves out the setup that make does.

Concretely, make allocates the hash table behind a map, sets up the send and receive queues on a channel, and gives a slice a backing array. Declare any of the three with var and you get nil instead.

The same map, declared both ways:

example.gogo
package main

import "fmt"

func main() {
	var counts map[string]int          // nil map, no hash table yet
	made := make(map[string]int)       // initialized, ready to write

	fmt.Println(counts == nil)         // true
	fmt.Println(made == nil)           // false

	made["/health"] = 1                // fine, made is initialized
	fmt.Println(made["/health"])       // 1

	// counts["/health"] = 1           // panic: assignment to entry in nil map
}

People often expect an allocation to hand back a pointer. make doesn't. It allocates and then gives you the value directly.

So make([]int, 3) returns a plain []int, not a *[]int, and you use it without dereferencing anything. make(map[string]int) returns a map you can write to immediately. The built-in that does return a pointer is new, covered further down.

var x Tmake(T, ...)
Works forany typeslices, maps, channels only
Gives youthe zero valuean initialized, usable value
Slice resultnil slice (len 0, cap 0)non-nil slice with backing array
Map resultnil map (write panics)writable map
Channel resultnil channel (blocks forever)usable channel
Returnsthe valuethe value, never a pointer

When should you use var?

Use var for any type whose zero value is already useful: numbers, booleans, strings, structs, pointers, and interfaces. Go is designed so the zero value is a valid starting state, which means you rarely have to initialize anything first.

bytes.Buffer is a good example. It has no constructor. You declare it with var and start writing:

example.gogo
package main

import (
	"bytes"
	"fmt"
)

func main() {
	var buf bytes.Buffer            // zero value is ready to use
	buf.WriteString("status=")
	buf.WriteString("ok")
	fmt.Println(buf.String())      // status=ok
}

sync.Mutex works the same way. Its zero value is an unlocked mutex, and there is no NewMutex constructor because you don't need one.

var also works for a slice, as long as you only grow it with append. A nil slice is a valid empty slice with length 0 and capacity 0, and append allocates a backing array when you add the first element. You can't index into it, though. ids[0] = 3 panics with index out of range because the length is still 0. If you need to assign by index, use make([]int, n) so the slice has a real length from the start. For collecting items with append, a nil slice is fine:

example.gogo
package main

import "fmt"

func main() {
	var ids []int                  // nil slice, but append-ready
	for i := 1; i <= 3; i++ {
		ids = append(ids, i*10)
	}
	fmt.Println(ids)               // [10 20 30]
	fmt.Println(ids == nil)        // false, append allocated it
}

Writing ids := make([]int, 0) here gains you nothing. The nil slice is shorter and behaves the same once the first append runs. If you want practice with this, the Composite Types course has a full set of exercises on slice and map behavior.

When should you use make?

Use make when a slice, map, or channel has to hold data before anything gets appended to it. In practice that means a map you're about to write keys into, a channel that goroutines will send on, or a slice you want pre-sized so its backing array exists up front.

Maps come up most often, because there's no append equivalent that allocates one for you. If you plan to assign any key, you have to make the map first. Counting requests per path in an HTTP handler is a typical case:

example.gogo
package main

import "fmt"

func main() {
	hits := make(map[string]int)   // must initialize before writing

	for _, path := range []string{"/api", "/api", "/health"} {
		hits[path]++                // safe: map is initialized
	}
	fmt.Println(hits["/api"])      // 2
	fmt.Println(hits["/health"])   // 1
}

Channels are similar. A channel you send on has to be made, because a nil channel blocks forever (more on that below). A worker fan-out starts with a made channel:

example.gogo
package main

import "fmt"

func main() {
	jobs := make(chan int, 3)      // buffered channel, capacity 3
	jobs <- 1
	jobs <- 2
	close(jobs)

	for j := range jobs {
		fmt.Println("processing job", j)
	}
}

For slices, make comes in two forms. make([]T, n) gives you a slice of length n filled with zero values, which you index into directly. make([]T, 0, n) gives you an empty slice with room for n elements, which you append into without reallocating. Use the first when you'll fill every slot by index, such as reading a fixed number of bytes:

example.gogo
package main

import "fmt"

func main() {
	// length 4, index into it directly
	buf := make([]byte, 4)
	copy(buf, []byte("data"))
	fmt.Println(string(buf))       // data

	// length 0, capacity 8, append without regrowing
	sums := make([]int, 0, 8)
	for i := 0; i < 8; i++ {
		sums = append(sums, i*i)
	}
	fmt.Println(sums)              // [0 1 4 9 16 25 36 49]
}

The second form is about performance. As a slice grows, append keeps copying it into bigger arrays. If you know roughly how many items are coming, setting the capacity up front skips those copies. The program's behavior doesn't change, only its speed.

Why does writing to a nil map panic but appending to a nil slice work?

The two were designed differently. append knows how to handle a nil slice. On the first call it allocates a backing array and returns a real slice, so writing through append always works. Maps have nothing like that. A write goes straight to a hash table, a nil map doesn't have one, and the runtime panics instead of guessing what you meant.

Most people hit this in their first week of Go. Here is what each nil type does, starting with the nil slice, which you can read and append to without problems:

example.gogo
package main

import "fmt"

func main() {
	var s []int                    // nil slice
	fmt.Println(len(s), cap(s))    // 0 0
	fmt.Println(s == nil)          // true

	s = append(s, 42)              // works: append allocates
	fmt.Println(s)                 // [42]
}

A nil map is different. Reading from it returns the value type's zero value and ranging over it does nothing, but the first write panics:

example.gogo
package main

import "fmt"

func main() {
	var m map[string]int           // nil map
	fmt.Println(m["missing"])      // 0, reading nil map is safe
	fmt.Println(len(m))            // 0, also safe

	m["key"] = 1                   // panic: assignment to entry in nil map
	fmt.Println(m)
}

That split between safe reads and panicking writes is why the bug is easy to miss. A nil map passes every test that only reads from it, then panics the first time real data comes through. The fix doesn't change: initialize it with make before the first write, or accept a map the caller already made.

Nil channels behave in the oddest way of the three. They don't panic. A send or receive on a nil channel just never proceeds, which in a single goroutine is a deadlock:

example.gogo
package main

func main() {
	var ch chan int                // nil channel
	ch <- 1                        // blocks forever: fatal deadlock
}

That sounds like a trap, but it's useful inside a select. A nil channel is never ready, so setting a channel variable to nil turns off its case, and you can disable a branch at runtime that way. Watch out for close though: closing a nil channel panics, so only close channels you made. The Concurrency Fundamentals course covers the select-with-nil-channels pattern in depth. The nil map and nil slice pair is also in our list of common Go mistakes to avoid, since experienced developers get caught by it too.

What about new? (var vs make vs new)

new is the third allocation built-in, and it does a different job from make. new(T) allocates zeroed storage for a T and returns a *T. make only handles slices, maps, and channels and returns the value. new works for any type and always returns a pointer.

The usual reason to call it is to get a pointer to a fresh zero value without declaring a named variable first:

example.gogo
package main

import "fmt"

func main() {
	p := new(int)                  // *int pointing at a zeroed int
	fmt.Println(*p)                // 0
	*p = 7
	fmt.Println(*p)                // 7
}

Since Go 1.26, new also accepts an expression. new(expr) allocates space, stores the expression's value there, and returns a pointer to it. That makes the old intPtr-style helpers, which only existed to get a pointer to a literal, unnecessary:

example.gogo
package main

import "fmt"

func main() {
	port := new(8080)              // *int pointing at 8080 (Go 1.26)
	enabled := new(true)           // *bool pointing at true

	fmt.Println(*port)             // 8080
	fmt.Println(*enabled)          // true
}

This is useful for optional struct fields, where a nil pointer means "unset" and a non-nil one means someone chose a value. The Pointers & Memory course has a config-defaulting exercise built on this pattern. The three built-ins side by side:

Built-inWorks forReturnsResult is usable?
var x Tany typethe zero valueusable if the zero value is (not for map writes)
make(T, ...)slice, map, channelthe value Tyes, initialized
new(T)any type*T, a pointerpoints at a zeroed T
new(expr) (Go 1.26)any expressionpointer to that valuepoints at the given value

For a slice, map, or channel you're about to fill, you almost always want make. Use new when you specifically need a pointer, most often a pointer to a literal for an optional field.

A quick decision rule

If you're not sure which one to use, go down this list and stop at the first line that matches:

  1. A map you will write keys into? Use make(map[K]V). A nil map panics on the first write.
  2. A channel goroutines will send on? Use make(chan T) or make(chan T, n). A nil channel blocks forever.
  3. A slice you want pre-sized, for speed or to assign by index? Use make([]T, n) or make([]T, 0, n).
  4. A slice you will only grow with append? Use var s []T. A nil slice works with append.
  5. A pointer to a fresh value? Use new(T), or new(expr) on Go 1.26 to point at a literal.
  6. Anything else (int, string, bool, struct, most standard library types)? Use var, because the zero value is ready to use.

If you remember one thing, make it this: before writing a key into a map, check where the map came from. A map passed in as a parameter was presumably initialized by the caller. A map you declared yourself with var is nil, and the first write will panic.

Frequently Asked Questions

Can I use var for a map?

You can declare a map with var m map[string]int. Reading from it, ranging over it, and calling len on it are all safe. Writing a key is not: m["x"] = 1 panics with assignment to entry in nil map. If you only read, var is fine. If you write any key, initialize the map with make first or take one the caller already built.

Is make([]int, 0) the same as var s []int?

Both have length 0 and behave the same once you append. The difference shows up when you compare with nil. var s []int is nil, while make([]int, 0) is a non-nil empty slice. That matters if your code or a test checks s == nil, or if you encode to JSON, where nil becomes null and the empty slice becomes []. For plain appending, prefer var.

Does make work for structs?

No. make only accepts slices, maps, and channels, so make(MyStruct) is a compile error. A struct doesn't need the runtime setup that make does. Create one with a composite literal like MyStruct{}, declare it with var s MyStruct to get the zero value, or use new(MyStruct) when you want a *MyStruct.

make vs new: which for a pointer?

Use new when you want a pointer. new(T) returns a *T pointing at a zeroed value, and on Go 1.26 new(expr) returns a pointer to that expression's value. make never returns a pointer. It returns an initialized slice, map, or channel. The two don't overlap: make gives you a usable collection and new gives you a pointer.

Sources

Primary references cited in this post (last verified July 3, 2026):

Write Go like a senior engineer

Interactive lessons in your browser. The first ones are free.

Try a free lessonOr create a free account