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 newmake 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. make works 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 nmake([]T, n)
Empty slice, length 0 but room for nmake([]T, 0, n)
Slice with length and capacitymake([]T, len, cap)
Empty mapmake(map[K]V)
Map pre-sized for n entriesmake(map[K]V, n)
Unbuffered channelmake(chan T)
Buffered channel, capacity nmake(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)

len is what you can index; cap is just reserved room. You still have to append (or grow len) 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 0 length. make([]int, 0, 10000) is empty and ready to append. 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 onslices, maps, channels onlyany type
Returnsan initialized value of type Ta pointer *T to a zeroed T
Result is usable?yes, ready to gozeroed — 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. new is 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{} vs var s []int: the literal is an empty-but-non-nil slice; var s []int is a nil slice. Both have len 0 and both accept append, so the difference rarely matters — except s == nil is true only for the second, which can surprise you in JSON (nilnull, []int{}[]).

Quick rules of thumb

  • Building a slice in a loop and know the size?make([]T, 0, n) then append.
  • Need n zero values right now?make([]T, n).
  • Any map you’ll write to?make(map[K]V) (or a literal) first — never a var nil map.
  • Synchronous handoff?make(chan T). Decouple sender/receiver?make(chan T, n).
  • Independent slice copy?make + copy, or slices.Clone.
  • Pointer to a struct?&T{}, not new or make.

Common mistakes

  • make([]T, n) when you meant make([]T, 0, n) — the first gives n zeros; appending grows past them. The second is empty with room reserved.
  • Writing to a nil mapvar m map[string]int; m["x"] = 1 panics. Reading is fine; writing needs make.
  • Using new for a slice/map — you get a pointer to a nil value that still isn’t usable. Use make.
  • Confusing len and cap — you can only index 0..len-1. Capacity beyond len is reserved, not accessible.
  • Sending on a nil channel — blocks forever. A channel must be maked before use.
  • make on a struct or array — won’t compile. make is slices, maps, and channels only.
Toggle theme (T)