The raw allocator versus the constructor
You are writing a Go program. You need a map to cache some data. You type cache := new(map[string]int). The compiler accepts it. You run the code and try to store a value: cache["key"] = 123. The program crashes immediately with assignment to entry in nil map.
You stare at the error. You checked the documentation. new allocates memory. make initializes slices, maps, and channels. You tried new because you wanted to create something. Now you have a pointer to a broken map and a panic.
This confusion is common. Go provides two built-in functions for creation, and they do fundamentally different things. new is a raw memory allocator. It gives you a pointer to zeroed memory. It does not know what the memory holds. make is a type-specific constructor. It builds the internal machinery required for slices, maps, and channels to function.
Understanding the difference prevents nil pointer panics and helps you write idiomatic Go.
What each function actually does
new(T) allocates enough memory to hold a value of type T, zeroes that memory, and returns a pointer of type *T. It works for any type. If you ask for new(int), you get a *int pointing to 0. If you ask for new([]int), you get a *[]int pointing to a nil slice.
make(T, args) initializes a value of type T and returns the value itself, not a pointer. It only works for three types: slices, maps, and channels. These types are complex. They are not just raw data. They are headers that point to internal structures managed by the runtime. make sets up those structures.
Think of new as a vending machine that dispenses a sealed envelope. Inside is a zero value. You get the envelope, which is a pointer to the contents. You can open the envelope to see the zero value, but the value itself might be useless.
Think of make as a factory that builds a working machine. You get the machine itself, running and ready. Slices, maps, and channels are machines. They need internal setup. new just gives you a box containing a broken machine (nil). make builds the machine.
Minimal examples
Here is the difference in action. new returns a pointer to a zero value. make returns an initialized value.
package main
import "fmt"
func main() {
// new allocates memory for a slice header and returns a pointer.
// The header fields are zeroed. The slice is nil.
// s is a *[]int pointing to a nil slice.
s := new([]int)
// make initializes the slice header and allocates the backing array.
// It returns the slice value directly.
// t is a []int with length 0 and capacity 5.
t := make([]int, 0, 5)
// new returns a pointer to a nil map.
// m is a *map[string]int.
// *m is nil.
m := new(map[string]int)
// make builds the map's internal hash table.
// It returns a map ready for reads and writes.
// n is a map[string]int.
n := make(map[string]int)
fmt.Printf("s type: %T, value: %v\n", s, s)
fmt.Printf("t type: %T, value: %v\n", t, t)
fmt.Printf("m type: %T, value: %v\n", m, m)
fmt.Printf("n type: %T, value: %v\n", n, n)
}
The output shows the types clearly. s and m are pointers. t and n are the values themselves. The values from new are nil. The values from make are initialized.
Inside the runtime
When you call new(T), the compiler translates it to a call to the runtime allocator. The runtime requests a block of memory, zeroes every byte, and returns the address. The runtime does not inspect the type T. It does not know if T is a struct, an int, or a slice. It just gives you zeroed bytes.
When you call make(T, args), the compiler checks the type. If T is not a slice, map, or channel, the compiler rejects the code with invalid use of make (non-slice/map/channel type T). If the type is valid, the compiler generates a call to a specific runtime function.
For slices, make calls makeslice. This function allocates the backing array and fills in the slice header with the pointer to the array, the length, and the capacity.
For maps, make calls makemap. This function allocates the hash table buckets and initializes the map header. It calculates the size based on the expected number of elements if you provide a capacity argument.
For channels, make calls makechan. This function allocates the circular buffer and sets up the send and receive queues.
make returns the header value directly. You do not get a pointer to the header. You get the header. This is why make returns []int, not *[]int.
Realistic usage and pitfalls
In real code, you almost always need make for collections. Using new for slices, maps, or channels is a recipe for runtime errors.
Here is a function that processes data. It uses make correctly.
// ProcessItems initializes a map to track counts and a channel for results.
// It demonstrates proper initialization of complex types.
func ProcessItems() {
// make builds the map's internal hash table.
// Without make, counts would be nil and cause a panic on assignment.
counts := make(map[string]int)
// make creates a buffered channel.
// The buffer allows sends to proceed without a receiver immediately.
results := make(chan string, 10)
// Simulate work.
counts["item"] = 1
results <- "done"
// Close channel when done sending.
close(results)
}
Now consider what happens if you use new by mistake.
func BadMap() {
// new returns a pointer to a nil map.
// m is a *map[string]int.
// *m is nil.
m := new(map[string]int)
// Dereference the pointer to get the nil map.
// Assigning to a nil map causes a runtime panic.
(*m)["key"] = 1
}
The runtime panics with assignment to entry in nil map. The map exists as a variable, but its internal structure is nil. You cannot write to it.
Channels behave differently. Sending on a nil channel does not panic. It blocks forever.
func BadChannel() {
// new returns a pointer to a nil channel.
// ch is a *chan int.
// *ch is nil.
ch := new(chan int)
// Sending on a nil channel blocks indefinitely.
// The program hangs with no output.
*ch <- 1
}
The program hangs. The goroutine waits for a receiver that will never come because the channel is not initialized. This is a silent deadlock. It is harder to debug than a panic.
Slices are slightly safer. The zero value of a slice is nil. You can call append on a nil slice. It works. However, new([]int) gives you a pointer to a nil slice. You have to dereference the pointer to use the slice. This adds unnecessary indirection.
func WeirdSlice() {
// new returns a pointer to a nil slice.
// s is a *[]int.
s := new([]int)
// Dereference to append.
// This works, but it is confusing and verbose.
*s = append(*s, 1)
// Idiomatic Go uses the zero value directly.
// var t []int is equivalent to t := nil.
// append works on nil slices.
var t []int
t = append(t, 1)
}
The idiomatic approach uses var t []int. It is simpler. It avoids the pointer. It relies on the fact that append handles nil slices correctly.
When to use what
The decision is straightforward. make is for collections. new is rarely needed.
Use make when you need a slice, map, or channel. It initializes the internal data structures so the value is ready to use.
Use a composite literal like &Struct{} when you need a pointer to a struct with specific fields. It is clearer than new(Struct).
Use the zero value directly when you just need a variable to hold a value later. Declare it with var s []int and let append or assignment handle the rest.
Use new only when you explicitly need a pointer to a zero value of a type that does not require initialization, and you prefer the brevity. This is rare in modern code. Most developers use &T{} instead.
Conventions and style
The Go community has strong conventions around allocation.
new is almost dead. You will rarely see it in idiomatic Go code. If you need a pointer to a struct, use &Struct{}. It is explicit. It shows the type and allows you to set fields in the same expression. new(Struct) hides the type behind the function call and forces you to set fields separately.
make arguments matter. For slices, make([]T, len, cap) sets the length and capacity. If you only need a slice to grow, use make([]T, 0, cap) or just var s []T. Setting the length to zero ensures append starts from the beginning. Setting the capacity pre-allocates memory to avoid reallocations.
Maps benefit from capacity hints. make(map[K]V, hint) allocates enough buckets for the expected number of elements. This reduces resizing overhead. If you do not know the size, omit the hint. make(map[K]V) is fine.
Zero values are safe for reading. A nil slice returns length and capacity of zero. A nil map returns the zero value for the value type on lookup. A nil channel blocks on send and receive. Use this knowledge to write defensive code. Check for nil maps before writing. Check for nil slices before accessing indices.
Summary
new allocates zeroed memory and returns a pointer. make initializes slices, maps, and channels and returns the value. new works for any type. make works only for collections.
Using new for collections gives you a pointer to a nil value. This leads to panics on map writes and deadlocks on channel sends. Use make for collections. Use composite literals for structs. Use zero values for simple variables.
new gives you a box. make gives you a tool.