Question

Go make() vs new(): When and Why to Use Each

Question

In Go, the introduction spends a lot of time explaining the difference between new() and make(). However, in practice, it seems possible to create values inside a local function scope and return them safely.

Why would you use new() or make() at all? When should each allocator be used, and why are they necessary if local variables can be returned from a function?

Short Answer

By the end of this page, you will understand why new() and make() exist in Go, what each one actually does, and why returning local values from a function is completely valid. You will also learn which types use make(), which types can use new(), and the practical situations where Go developers choose one approach over another.

Concept

In Go, new() and make() solve different problems.

  • new(T) allocates zeroed storage for a value of type T and returns *T.
  • make(T, ...) creates and initializes certain built-in reference-like types and returns a value of type T, not *T.

What new() does

new(T) gives you a pointer to a zero value of type T.

p := new(int)
fmt.Println(*p) // 0

This is roughly similar to:

var x int
p := &x

So new() is not magic memory management that makes values survive after a function returns. Go already allows that safely.

What make() does

make() is only for these three built-in types:

  • slices
  • maps
  • channels

These types need internal runtime initialization before they are useful.

s := make([]int, 3)
m := make(map[string]int)
ch := make(chan int)

Without make(), these types may exist as zero values, but some of them are not ready for use in the way you usually need.

For example:

var m map[string]int
// m is nil
// m["a"] = 1 // panic: assignment to entry in nil map

You need make(map[string]int) before writing into the map.

Why local variables can still be returned

A common beginner confusion is thinking that local variables disappear in an unsafe way when a function ends. In Go, this is not a problem.

func makeUser() *User {
    u := User{Name: "Ana"}
    return &u
}

This is safe. If a value must outlive the function call, Go's compiler and runtime ensure it is stored appropriately. You do not manually manage stack vs heap memory in normal Go code.

So the purpose of new() and make() is not "keep this alive after return." Their purpose is:

  • new() → get a pointer to a zero value
  • make() → create a ready-to-use slice, map, or channel

Why this matters in real programming

Understanding this helps you write clearer Go code:

  • You avoid using new() when a simple variable is clearer.
  • You know that maps usually need make() before insertion.
  • You know slices and channels are usually created with make().
  • You stop worrying about whether returning local values is safe.

This makes your code more idiomatic and easier to read.

Mental Model

Think of Go values as different kinds of containers.

  • new() gives you the address of an empty box.
  • make() gives you a box that is already assembled and ready for use for special container types.

Analogy

Imagine three situations:

  • new(int) is like getting a numbered parking spot with a car-sized empty space in it. You now have a pointer to that spot.
  • make([]int, 3) is like getting an actual shelf with 3 usable slots already set up.
  • Returning a local variable from a function is like handing someone the shelf before leaving the room. Go makes sure it remains available.

The key idea is:

  • new() allocates storage
  • make() initializes special built-in data structures
  • returning locals is safe because Go handles the memory details for you
Take Quiz

Syntax and Examples

Core syntax

new()

p := new(T)
  • Allocates zeroed storage for type T
  • Returns *T

Example:

package main

import "fmt"

func main() {
    count := new(int)
    fmt.Println(*count) // 0

    *count = 42
    fmt.Println(*count) // 42
}

Here, count is a pointer to an int.

make()

s := make([]int, len, cap)
m := make(map[string]int)
ch := make(chan int)
  • Only works with slices, maps, and channels
  • Returns the type itself, not a pointer

Example:

package main

import "fmt"

func main() {
    scores := make(map[string]int)
    scores["Alice"] = 90

    nums := make([]int, 3)
    nums[0] = 10

    fmt.Println(scores)
    fmt.Println(nums)
}

Returning local values

package main

import "fmt"

type User struct {
    Name string
}

func createUser() *User {
    u := User{Name: "Mia"}
    return &u
}

func main() {
    user := createUser()
    fmt.Println(user.Name)
}

This works safely. You do not need new() just because a value is created in a function.

Idiomatic alternative to new()

For structs, Go code often uses:

u := &User{Name: "Mia"}

instead of:

u := new(User)
u.Name = "Mia"

Both work, but the composite literal form is often clearer when you already know field values.

Step by Step Execution

Consider this example:

package main

import "fmt"

func build() (*int, map[string]int) {
    n := 7
    m := make(map[string]int)
    m["value"] = n
    return &n, m
}

func main() {
    p, m := build()
    fmt.Println(*p)
    fmt.Println(m["value"])
}

Step by step

Inside build()

  1. n := 7

    • A local integer variable is created with value 7.
  2. m := make(map[string]int)

    • A usable empty map is created.
    • This map is initialized and ready to store key-value pairs.
  3. m["value"] = n

    • The map now contains one entry: "value" -> 7.
  4. return &n, m

    • The function returns a pointer to n and the map m.
    • This is safe, even though n is local.

Inside main()

  1. p, m := build()

    • p receives the pointer to the integer.
    • m receives the map.
  2. fmt.Println(*p)

    • Dereferences the pointer and prints 7.
  3. fmt.Println(m["value"])

    • Looks up "value" in the map and prints 7.

Important lesson

The pointer works because Go ensures the value remains valid after the function returns. The map works because make() created a properly initialized map.

If we had written this instead:

var m map[string]int
m["value"] = 7

it would panic, because a nil map cannot accept assignments.

Real World Use Cases

Common practical uses of make()

Building maps for lookups

userAges := make(map[string]int)
userAges["Sam"] = 31

Used in:

  • caching data
  • counting occurrences
  • grouping records
  • configuration lookups

Creating slices with known size

results := make([]string, 0, 100)

Used in:

  • collecting database rows
  • building API response lists
  • processing files line by line

Creating channels for goroutines

jobs := make(chan string)

Used in:

  • worker pools
  • background processing
  • passing data safely between goroutines

Common practical uses of new()

When you specifically need a pointer to a zero value

flag := new(bool)

This can be useful when:

  • an API expects a pointer
  • you want to distinguish between a missing value and a set value
  • you need a pointer field in a struct

Common practical use of local values and returning them

func NewConfig() *Config {
    cfg := Config{Port: 8080}
    return &cfg
}

This is very common in constructors and factory-style functions. The local variable is safe to return.

Take Quiz

Real Codebase Usage

In real Go codebases, developers usually choose the clearest form rather than always reaching for new().

Common patterns

Composite literals for structs

type Server struct {
    Host string
    Port int
}

func NewServer() *Server {
    return &Server{Host: "localhost", Port: 8080}
}

This is more common than:

s := new(Server)
s.Host = "localhost"
s.Port = 8080
return s

make() before writing to a map

func countWords(words []string) map[string]int {
    counts := make(map[string]int)
    for _, w := range words {
        counts[w]++
    }
    return counts
}

Preallocating slices for performance and clarity

items := make([]string, 0, len(input))

This is common when developers know approximately how many items will be appended.

Channels for coordination

done := make(chan struct{})

Often used for:

  • cancellation signals
  • shutdown notifications
  • goroutine synchronization

Guarding against nil maps

Developers often initialize maps immediately in constructors or setup code so later functions can safely write to them.

type Store struct {
    cache map[string]string
}

func NewStore() *Store {
    return &Store{
        cache: make(map[string]string),
    }
}

This avoids runtime panics later.

Common Mistakes

1. Using new(map[...]) and expecting a usable map

Broken code:

m := new(map[string]int)
(*m)["a"] = 1

Why it fails:

  • new(map[string]int) returns *map[string]int
  • the map value inside is still nil
  • writing to a nil map causes a panic

Correct code:

m := make(map[string]int)
m["a"] = 1

2. Using new([]int) when you really want a slice

Broken idea:

s := new([]int)
fmt.Println(len(*s)) // 0, but not very useful

This gives you a pointer to a nil slice, not an initialized working slice with length or capacity.

Usually you want:

s := make([]int, 3)

or:

s := []int{}

3. Thinking local variables cannot be returned safely

Broken assumption:

func badIdea() *int {
    x := 5
    return &x // some beginners think this is invalid
}

This is actually valid in Go.

4. Overusing new() for structs

Less clear:

u := new(User)
u.Name = "Eva"

Usually clearer:

u := &User{Name: "Eva"}

5. Forgetting that nil slices and nil maps behave differently

var s []int
s = append(s, 1) // works

var m map[string]int
m["a"] = 1 // panic

How to avoid confusion:

  • nil slices can often still be appended to
  • nil maps must be initialized with make() before assignment

6. Expecting make() to work for structs

Broken code:

type User struct {
    Name string
}

u := make(User) // invalid

make() only works for:

  • slices
  • maps
  • channels

Comparisons

new() vs make()

Featurenew()make()
Works withAny typeOnly slice, map, channel
ReturnsPointer to type (*T)The type itself (T)
Initializes internal runtime dataNoYes
Common useGet pointer to zero valueCreate usable slice/map/channel

new(User) vs &User{}

FormMeaningCommon use
new(User)Pointer to zero-value UserWhen you want all zero values
&User{}Pointer to a new User literalVery common and idiomatic
&User{Name: "Ava"}Pointer with fields setBest when initializing fields

Nil map vs made map

FormCan read?Can write?
var m map[string]intYes, returns zero valueNo, panics
make(map[string]int)YesYes

Nil slice vs made slice

FormLengthCan append?
var s []int0Yes
make([]int, 0)0Yes
make([]int, 3)3Yes
Take Quiz

Cheat Sheet

Quick reference

Use new() when

  • You need a *T
  • You want a pointer to a zero value
p := new(int)   // *int
u := new(User)  // *User

Use make() when

  • You need a usable slice, map, or channel
s := make([]int, 5)
m := make(map[string]int)
ch := make(chan string)

Important rules

  • new(T) returns *T
  • make(T, ...) returns T
  • make() only works for slices, maps, and channels
  • Returning pointers to local variables is safe in Go
  • Nil map: readable, not writable
  • Nil slice: append usually works

Common idioms

u := &User{Name: "Lee"}     // preferred for structs
m := make(map[string]int)    // before writing
s := make([]string, 0, 10)   // preallocate capacity

Avoid these mistakes

new(map[string]int) // pointer to nil map, usually not what you want
make(User)          // invalid

FAQ

Why does Go have both new() and make()?

Because they do different jobs. new() allocates zeroed storage and returns a pointer. make() creates usable slices, maps, and channels.

Can I return a pointer to a local variable in Go?

Yes. Go makes this safe automatically.

Why can't I write to a nil map?

A nil map has no initialized internal storage for entries. Use make(map[K]V) first.

Why can I append to a nil slice but not write to a nil map?

Slices and maps have different runtime behavior. append can allocate a new backing array for a nil slice, but map assignment requires an initialized map.

Should I use new() for structs?

Usually, &Struct{} or &Struct{Field: value} is clearer and more idiomatic.

Does make() allocate on the heap?

You usually do not need to care. Go decides stack or heap placement automatically based on usage.

Can make() be used with arrays or structs?

No. make() is only for slices, maps, and channels.

Take Quiz

Mini Project

Description

Build a small Go program that manages a simple word counter. This project demonstrates when to use make() for maps and slices, and shows that local variables can be safely returned from functions.

Goal

Create a program that counts word frequency, returns the result from a helper function, and prints the counts.

Requirements

  1. Create a function that accepts a slice of words.
  2. Use a map to count how many times each word appears.
  3. Initialize the map correctly so assignments do not panic.
  4. Return the map from the function.
  5. In main(), call the function and print the results.
Take Quiz

Keep learning

Related questions

View more