make is the built-in that creates and initializes the three types that need internal bookkeeping: slices, maps, and channels. It’s not interchangeable with new — make returns a ready-to-use value of the type itself, not a pointer, and it’s the only way to get a usable map or channel.
s := make([]int, 5) // slice
m := make(map[string]int) // map
ch := make(chan int) // channel
Only these three types.
makeworks on slices, maps, and channels — nothing else. For everything else (structs, arrays, ints…) the zero value or a composite literal is all you need.
At a glance
| Want to… | Use |
|---|---|
Empty slice, length n | make([]T, n) |
Empty slice, length 0 but room for n | make([]T, 0, n) |
| Slice with length and capacity | make([]T, len, cap) |
| Empty map | make(map[K]V) |
Map pre-sized for n entries | make(map[K]V, n) |
| Unbuffered channel | make(chan T) |
Buffered channel, capacity n | make(chan T, n) |
Slices
The three forms
a := make([]int, 5) // len=5, cap=5 → [0 0 0 0 0]
b := make([]int, 0, 5) // len=0, cap=5 → [] (5 slots reserved)
c := make([]int, 3, 10) // len=3, cap=10 → [0 0 0]
- Second arg = length — how many elements exist right now (all set to the zero value).
- Third arg = capacity — how many the underlying array can hold before it must grow. Optional; defaults to the length.
s := make([]int, 3, 10)
len(s) // 3
cap(s) // 10
s[0] // 0 ✅ indices 0..2 are valid
s[3] // ❌ panic: index out of range (len is 3, not cap)
lenis what you can index;capis just reserved room. You still have toappend(or growlen) to reach the extra capacity.
Why pre-size with capacity
append re-allocates and copies the whole backing array each time it runs out of room. If you know the final size, reserve it up front and skip all the intermediate copies:
// ❌ grows and copies repeatedly
out := []int{}
for i := 0; i < 10000; i++ {
out = append(out, i)
}
// ✅ one allocation, zero re-copies
out := make([]int, 0, 10000)
for i := 0; i < 10000; i++ {
out = append(out, i)
}
Note the
0length.make([]int, 0, 10000)is empty and ready toappend.make([]int, 10000)gives you 10000 zeros, so appending adds a 10001st element — a classic bug.
make + copy — an independent copy
src := []int{1, 2, 3}
dst := make([]int, len(src))
copy(dst, src) // dst is a separate array; mutating it won't touch src
(For Go 1.21+, slices.Clone(src) does the same thing more concisely — see the slices cheatsheet.)
Maps
A map must be made (or be a literal) before you write to it — the zero value of a map is nil, and writing to a nil map panics.
m := make(map[string]int)
m["x"] = 1 // ✅
var n map[string]int // nil map
n["x"] = 1 // ❌ panic: assignment to entry in nil map
_ = n["x"] // ✅ reading a nil map is fine — returns the zero value
Size hint
The optional second argument is a hint for how many entries you expect. It doesn’t cap the map — it just pre-allocates buckets to avoid re-hashing as it grows.
counts := make(map[string]int, 1000) // expecting ~1000 keys
It’s only a hint. The map still grows past it on demand; you’re just saving the cost of incremental resizing.
See the maps cheatsheet for idioms once the map exists (comma-ok, map-as-set, etc.).
Channels
unbuf := make(chan int) // unbuffered — send blocks until a receiver is ready
buf := make(chan int, 3) // buffered — holds 3 values before send blocks
- No size / size 0 → unbuffered: every send waits for a matching receive (synchronous handoff).
- Size
n→ buffered: sends only block when the buffer is full; receives only block when it’s empty.
var ch chan int // nil channel
ch <- 1 // ❌ blocks forever (deadlock) — nil channels never proceed
See the Concurrency cheatsheet for select, ranging over channels, and closing.
make vs new
The two are easy to confuse. They are not the same:
make(T, ...) | new(T) | |
|---|---|---|
| Works on | slices, maps, channels only | any type |
| Returns | an initialized value of type T | a pointer *T to a zeroed T |
| Result is usable? | yes, ready to go | zeroed — a *map/*slice still points to nil |
s := make([]int, 3) // []int, ready: [0 0 0]
p := new([]int) // *[]int pointing to a nil slice; *p is still nil
Rule of thumb: for slices/maps/channels reach for
make.newis rarely used —&T{}is the idiomatic way to get a pointer to a struct.
make vs composite literal {}
For slices and maps, a composite literal does the same job as make when you also want initial contents:
// Empty + ready
a := make([]int, 0) // ≡ a := []int{}
m := make(map[string]int) // ≡ m := map[string]int{}
// With initial values — only the literal can do this
b := []int{1, 2, 3}
n := map[string]int{"a": 1, "b": 2}
Reach for make specifically when you want to set a length or capacity (make([]int, 0, 64)) — a literal can’t express that.
[]int{}vsvar s []int: the literal is an empty-but-non-nil slice;var s []intis a nil slice. Both havelen 0and both acceptappend, so the difference rarely matters — excepts == nilistrueonly for the second, which can surprise you in JSON (nil→null,[]int{}→[]).
Quick rules of thumb
- Building a slice in a loop and know the size? →
make([]T, 0, n)thenappend. - Need
nzero values right now? →make([]T, n). - Any map you’ll write to? →
make(map[K]V)(or a literal) first — never avarnil map. - Synchronous handoff? →
make(chan T). Decouple sender/receiver? →make(chan T, n). - Independent slice copy? →
make+copy, orslices.Clone. - Pointer to a struct? →
&T{}, notnewormake.
Common mistakes
make([]T, n)when you meantmake([]T, 0, n)— the first givesnzeros; appending grows past them. The second is empty with room reserved.- Writing to a nil map —
var m map[string]int; m["x"] = 1panics. Reading is fine; writing needsmake. - Using
newfor a slice/map — you get a pointer to anilvalue that still isn’t usable. Usemake. - Confusing
lenandcap— you can only index0..len-1. Capacity beyondlenis reserved, not accessible. - Sending on a nil channel — blocks forever. A channel must be
maked before use. makeon a struct or array — won’t compile.makeis slices, maps, and channels only.