You want to create a slice:
numbers := make([]int, 3)
But for a struct, you might see:
user := new(User)
Both seem to "create" something.
So why does Go need two built-in functions?
The answer is that make and new solve two very different problems.
The Concept
The simplest distinction is:
-
new(T)gives you a pointer to the zero value ofT -
make(T, ...)initializes a slice, map, or channel so it can actually be used
new works with almost any type.
make works only with:
slice
map
channel
Their return values are also different.
p := new(int)
Here:
p // *int
But:
s := make([]int, 3)
Here:
s // []int
make does not return a pointer to the slice.
It returns the initialized slice value itself.
Why Isn't the Zero Value Enough?
For many Go types, the zero value is immediately useful.
For example:
var n int
fmt.Println(n) // 0
And:
var u User
already gives you a valid zero-valued User.
But slices, maps, and channels are special.
Their zero values are nil.
var s []int
var m map[string]int
var ch chan int
Conceptually:
s = nil
m = nil
ch = nil
Sometimes that is useful.
But often you need their internal runtime state initialized before doing real work.
That is what make is for.
Small Runnable Example
package main
import "fmt"
type User struct {
Name string
}
func main() {
n := new(int)
fmt.Println(*n)
user := new(User)
fmt.Printf("%+v\n", *user)
numbers := make([]int, 3)
numbers[0] = 10
fmt.Println(numbers)
users := make(map[string]int)
users["alice"] = 42
fmt.Println(users)
jobs := make(chan string, 1)
jobs <- "send email"
fmt.Println(<-jobs)
}
Run it with:
go run main.go
Output:
0
{Name:}
[10 0 0]
map[alice:42]
send email
Notice the difference.
new(int) returned an *int pointing to:
0
new(User) returned a *User pointing to:
User{}
But make initialized the internal structures needed by the slice, map, and channel.
What Actually Happens?
new
Consider:
p := new(int)
You can think of it approximately like this:
new(int)
int value
┌─────┐
p ─► 0
└─────┘
The expression returns:
*int
The value being pointed to is the zero value of int.
Similarly:
u := new(User)
gives you:
*User
whose value initially contains zero values for all its fields.
One important detail: new does not mean "allocate this on the heap."
The Go compiler performs escape analysis and decides whether the storage can remain on the stack or needs to escape to the heap.
So this:
new(User)
should not be interpreted as:
force heap allocation
make
Slices, maps, and channels require more initialization.
For example:
s := make([]int, 3, 5)
A slice is not the underlying array itself.
Conceptually, it contains something like:
slice
├── pointer ─────► underlying array
├── len = 3
└── cap = 5
make creates the necessary state for that slice.
Similarly:
m := make(map[string]int)
initializes a map so entries can be added.
And:
ch := make(chan int, 10)
initializes a channel with a buffer capacity of 10.
This is why make exists specifically for these three built-in reference-like types.
A Common Mistake: new(map[...])
Suppose you want a map.
You might write:
users := new(map[string]int)
(*users)["alice"] = 42
It compiles.
But it panics:
panic: assignment to entry in nil map
Why?
Because:
new(map[string]int)
creates a pointer to a zero-valued map.
And the zero value of a map is:
nil
Conceptually:
users
|
v
┌─────────┐
│ nil map │
└─────────┘
The pointer exists.
The map itself still hasn't been initialized.
You could technically do this:
users := new(map[string]int)
*users = make(map[string]int)
(*users)["alice"] = 42
But that is unnecessarily complicated.
Just write:
users := make(map[string]int)
users["alice"] = 42
What About Slices?
Slices have an interesting difference.
A nil slice can still be used with append:
var numbers []int
numbers = append(numbers, 10)
numbers = append(numbers, 20)
fmt.Println(numbers)
This works.
So you do not always need:
make([]int, 0)
A nil slice is often perfectly idiomatic.
But make becomes useful when you already know the desired length or capacity.
For example:
items := make([]Item, 0, 100)
If you expect roughly 100 items, providing capacity can avoid some backing-array growth while appending.
Nil Maps and Nil Channels Behave Differently
The zero values of slices, maps, and channels are all nil, but they do not behave identically.
Nil slice
You can append:
var s []int
s = append(s, 1)
Valid.
Nil map
Reading is valid:
var m map[string]int
fmt.Println(m["missing"]) // 0
But writing panics:
m["key"] = 1
Nil channel
Sending or receiving on a nil channel blocks indefinitely:
var ch chan int
ch <- 1
The goroutine will block.
This is another reason initialization with make matters when you actually need to use maps or channels.
Practical Example
Imagine a small worker service.
You need:
- configuration
- a job queue
- a cache
type Config struct {
WorkerCount int
}
type Worker struct {
config *Config
jobs chan string
cache map[string]string
}
func NewWorker() *Worker {
return &Worker{
config: &Config{
WorkerCount: 4,
},
jobs: make(chan string, 100),
cache: make(map[string]string),
}
}
Here, make is appropriate for:
jobs
cache
because the worker needs an initialized channel and map.
For the struct itself, Go code commonly uses:
&Worker{...}
instead of:
new(Worker)
because the composite literal allows fields to be initialized immediately.
When Should You Use new?
new is useful when you specifically need a pointer to a zero value.
For example:
timeout := new(int)
But in normal application code, it is often less common than newcomers expect.
For structs, this:
user := &User{}
is usually at least as clear as:
user := new(User)
And if fields need initialization:
user := &User{
Name: "Alice",
}
is much more useful.
new becomes particularly convenient in generic code or situations where obtaining *T for an arbitrary type is useful.
When Should You Use make?
Use make when initializing:
[]T
map[K]V
chan T
Typical examples include:
users := make(map[string]User)
buffer := make([]byte, 4096)
jobs := make(chan Job, 100)
You do not need to call make automatically for every slice.
For example:
var results []Result
results = append(results, result)
is completely valid and idiomatic.
Use make when you actually need initialization parameters such as length, capacity, or channel buffering—or when a non-nil map/channel is required.
A Simple Mental Model
Think about the two functions this way:
new(T)
|
v
zero value of T
|
v
return *T
While:
make(slice/map/channel)
|
v
initialize runtime structure
|
v
return the value itself
So:
new(T)
is mostly about obtaining a pointer.
While:
make(...)
is about initializing Go's built-in slice, map, and channel data structures.
Takeaways
-
new(T)returns a*Tpointing to the zero value ofT. -
makeonly works with slices, maps, and channels. -
makereturns the initialized value itself, not*T. -
new(map[K]V)does not initialize the map; it gives you a pointer to a nil map. - In everyday Go code,
&Struct{}is often more useful thannew(Struct).
Top comments (0)