DEV Community

Triệu Mẫn Trà
Triệu Mẫn Trà

Posted on

[Golang] Why Go Has Both `make` and `new`

You want to create a slice:

numbers := make([]int, 3)
Enter fullscreen mode Exit fullscreen mode

But for a struct, you might see:

user := new(User)
Enter fullscreen mode Exit fullscreen mode

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 of T
  • 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
Enter fullscreen mode Exit fullscreen mode

Their return values are also different.

p := new(int)
Enter fullscreen mode Exit fullscreen mode

Here:

p // *int
Enter fullscreen mode Exit fullscreen mode

But:

s := make([]int, 3)
Enter fullscreen mode Exit fullscreen mode

Here:

s // []int
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

And:

var u User
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Conceptually:

s  = nil
m  = nil
ch = nil
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

Run it with:

go run main.go
Enter fullscreen mode Exit fullscreen mode

Output:

0
{Name:}
[10 0 0]
map[alice:42]
send email
Enter fullscreen mode Exit fullscreen mode

Notice the difference.

new(int) returned an *int pointing to:

0
Enter fullscreen mode Exit fullscreen mode

new(User) returned a *User pointing to:

User{}
Enter fullscreen mode Exit fullscreen mode

But make initialized the internal structures needed by the slice, map, and channel.


What Actually Happens?

new

Consider:

p := new(int)
Enter fullscreen mode Exit fullscreen mode

You can think of it approximately like this:

new(int)

    int value
   ┌─────┐
p ─►  0
   └─────┘
Enter fullscreen mode Exit fullscreen mode

The expression returns:

*int
Enter fullscreen mode Exit fullscreen mode

The value being pointed to is the zero value of int.

Similarly:

u := new(User)
Enter fullscreen mode Exit fullscreen mode

gives you:

*User
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

should not be interpreted as:

force heap allocation
Enter fullscreen mode Exit fullscreen mode

make

Slices, maps, and channels require more initialization.

For example:

s := make([]int, 3, 5)
Enter fullscreen mode Exit fullscreen mode

A slice is not the underlying array itself.

Conceptually, it contains something like:

slice
 ├── pointer ─────► underlying array
 ├── len = 3
 └── cap = 5
Enter fullscreen mode Exit fullscreen mode

make creates the necessary state for that slice.

Similarly:

m := make(map[string]int)
Enter fullscreen mode Exit fullscreen mode

initializes a map so entries can be added.

And:

ch := make(chan int, 10)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

It compiles.

But it panics:

panic: assignment to entry in nil map
Enter fullscreen mode Exit fullscreen mode

Why?

Because:

new(map[string]int)
Enter fullscreen mode Exit fullscreen mode

creates a pointer to a zero-valued map.

And the zero value of a map is:

nil
Enter fullscreen mode Exit fullscreen mode

Conceptually:

users
  |
  v
┌─────────┐
│ nil map │
└─────────┘
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

But that is unnecessarily complicated.

Just write:

users := make(map[string]int)
users["alice"] = 42
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

This works.

So you do not always need:

make([]int, 0)
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

Valid.

Nil map

Reading is valid:

var m map[string]int

fmt.Println(m["missing"]) // 0
Enter fullscreen mode Exit fullscreen mode

But writing panics:

m["key"] = 1
Enter fullscreen mode Exit fullscreen mode

Nil channel

Sending or receiving on a nil channel blocks indefinitely:

var ch chan int

ch <- 1
Enter fullscreen mode Exit fullscreen mode

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),
    }
}
Enter fullscreen mode Exit fullscreen mode

Here, make is appropriate for:

jobs
cache
Enter fullscreen mode Exit fullscreen mode

because the worker needs an initialized channel and map.

For the struct itself, Go code commonly uses:

&Worker{...}
Enter fullscreen mode Exit fullscreen mode

instead of:

new(Worker)
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

But in normal application code, it is often less common than newcomers expect.

For structs, this:

user := &User{}
Enter fullscreen mode Exit fullscreen mode

is usually at least as clear as:

user := new(User)
Enter fullscreen mode Exit fullscreen mode

And if fields need initialization:

user := &User{
    Name: "Alice",
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Typical examples include:

users := make(map[string]User)
Enter fullscreen mode Exit fullscreen mode
buffer := make([]byte, 4096)
Enter fullscreen mode Exit fullscreen mode
jobs := make(chan Job, 100)
Enter fullscreen mode Exit fullscreen mode

You do not need to call make automatically for every slice.

For example:

var results []Result
results = append(results, result)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

While:

make(slice/map/channel)
          |
          v
initialize runtime structure
          |
          v
return the value itself
Enter fullscreen mode Exit fullscreen mode

So:

new(T)
Enter fullscreen mode Exit fullscreen mode

is mostly about obtaining a pointer.

While:

make(...)
Enter fullscreen mode Exit fullscreen mode

is about initializing Go's built-in slice, map, and channel data structures.


Takeaways

  • new(T) returns a *T pointing to the zero value of T.
  • make only works with slices, maps, and channels.
  • make returns 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 than new(Struct).

Top comments (0)