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?
- When should you use var?
- When should you use make?
- Why does writing to a nil map panic but appending to a nil slice work?
- What about new? (var vs make vs new)
- A quick decision rule
- Frequently Asked Questions
- Sources
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.gogopackage 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 T | make(T, ...) | |
|---|---|---|
| Works for | any type | slices, maps, channels only |
| Gives you | the zero value | an initialized, usable value |
| Slice result | nil slice (len 0, cap 0) | non-nil slice with backing array |
| Map result | nil map (write panics) | writable map |
| Channel result | nil channel (blocks forever) | usable channel |
| Returns | the value | the 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.gogopackage 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.gogopackage 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.gogopackage 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.gogopackage 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.gogopackage 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.gogopackage 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.gogopackage 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.gogopackage 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.gogopackage 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.gogopackage 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-in | Works for | Returns | Result is usable? |
|---|---|---|---|
var x T | any type | the zero value | usable if the zero value is (not for map writes) |
make(T, ...) | slice, map, channel | the value T | yes, initialized |
new(T) | any type | *T, a pointer | points at a zeroed T |
new(expr) (Go 1.26) | any expression | pointer to that value | points 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:
- A map you will write keys into? Use
make(map[K]V). A nil map panics on the first write. - A channel goroutines will send on? Use
make(chan T)ormake(chan T, n). A nil channel blocks forever. - A slice you want pre-sized, for speed or to assign by index? Use
make([]T, n)ormake([]T, 0, n). - A slice you will only grow with
append? Usevar s []T. A nil slice works withappend. - A pointer to a fresh value? Use
new(T), ornew(expr)on Go 1.26 to point at a literal. - 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):
- The Go Programming Language Specification: Making slices, maps and channels, retrieved 2026-07-03
- The Go Programming Language Specification: Allocation (new), retrieved 2026-07-03
- The Go Programming Language Specification: The zero value, retrieved 2026-07-03
- Effective Go: Allocation with make and new, retrieved 2026-07-03
- Go 1.26 Release Notes: changes to the language, retrieved 2026-07-03
