How to Use the new() Function in Go

The `new()` function in Go allocates zeroed memory for a type and returns a pointer to it, whereas `make()` is used only for slices, maps, and channels to initialize their internal data structures.

The blank slate of Go allocation

You're writing a Go program and you need a pointer to a struct. You reach for new() because that's what you did in C or Java. Then you see a senior engineer write &User{Name: "Alice"} and you wonder why they didn't use new(). Or worse, you try new([]int) and get a pointer to a nil slice that panics when you try to append. The distinction between new(), make(), and composite literals is one of the first walls new Go developers hit.

new() is a built-in function that allocates zeroed memory for a type and returns a pointer to it. It does not run constructors. It does not initialize internal data structures. It just gives you a blank slate. Understanding when that blank slate is useful, and when it's a trap, is key to writing idiomatic Go.

What new() actually does

new(T) takes a type T. It allocates memory for a value of that type. It sets every byte of that memory to zero. It returns a pointer *T. That is the entire contract.

Think of new() like a factory that stamps out blank forms. It hands you a reference to the form. Every field is empty. The form exists, but it has no data. If the form has a signature line, it's blank. If it has a date field, it's empty. You get the paper, not the content.

This behavior is consistent across all types. new(int) gives you a pointer to an integer set to zero. new(string) gives you a pointer to an empty string. new(User) gives you a pointer to a User struct where every field is its zero value.

The function works for any type. You can pass a struct, a primitive, an array, or even a function type. The compiler accepts it. The runtime allocates and zeroes. The return value is always a pointer.

Minimal allocation

Here's the simplest usage: allocate a pointer to a zeroed struct and inspect the result.

package main

import "fmt"

type Config struct {
    Debug bool
    Port  int
}

func main() {
    // new() allocates memory for Config, sets all fields to zero values, and returns *Config
    c := new(Config)
    // c is a pointer. The struct exists with Debug=false and Port=0
    fmt.Printf("%T %+v\n", c, *c)
}
# output:
*Config {Debug:false Port:0}

The output shows *Config. The value is {Debug:false Port:0}. new() gave you a pointer to a struct where every field is zeroed. bool zero value is false. int zero value is 0.

You can assign to the fields through the pointer. The memory is allocated and ready.

package main

import "fmt"

type Config struct {
    Debug bool
    Port  int
}

func main() {
    c := new(Config)
    // Assign through the pointer. The memory is already allocated.
    c.Debug = true
    c.Port = 8080
    fmt.Printf("%+v\n", *c)
}
# output:
{Debug:true Port:8080}

This works, but it's verbose. You allocate, then you assign. Go offers a better way for structs.

The zero value contract

Go relies on zero values. Every type has a default zero value. int is 0. string is "". bool is false. pointer is nil. slice is nil. map is nil. channel is nil.

Well-designed Go types make the zero value useful. If you create a type and its zero value is broken, the type is poorly designed. new() exposes this contract. If new(T) gives you a value you can't use, T probably needs a constructor or a different design.

Consider a slice. A slice is a descriptor with three fields: a pointer to the underlying array, a length, and a capacity. new([]int) allocates memory for this descriptor and zeroes it. The pointer becomes nil. The length becomes 0. The capacity becomes 0. You get a pointer to a nil slice.

A nil slice has no underlying array. If you try to write to it, the program crashes.

package main

func main() {
    // new() returns *[]int. The slice header is zeroed, so the data pointer is nil.
    s := new([]int)
    // Dereference to get the slice, then try to assign.
    (*s)[0] = 42
}

The program panics with panic: runtime error: assignment to entry in nil map for maps, or a slice bounds panic for slices. The error message varies by operation, but the cause is the same: you have a nil data structure. new() gave you memory, but it didn't initialize the internal structure.

This is why new() is dangerous for collections. Collections need initialization. They need an underlying array or hash table allocated. new() doesn't do that. make() does.

make() is a built-in function for slices, maps, and channels. It allocates the internal data structure and returns the value itself, not a pointer. make([]int, 5) returns a []int with an underlying array of length 5. new([]int) returns a *[]int pointing to a nil slice.

The return types differ. make() returns T. new() returns *T. If you try to use new() where a value is expected, the compiler rejects it.

package main

func main() {
    // s expects []int. new([]int) returns *[]int.
    s := new([]int)
    // The compiler rejects this with cannot use new([]int) (value of type *[]int) as []int value in assignment
    _ = s
}

The error is clear. You have a pointer, not a slice. You can dereference, but you'll get a nil slice. The fix is to use make().

Realistic usage: types that work at zero

new() shines for types where the zero value is fully functional. These types don't need initialization. They work immediately after allocation.

The standard library has several such types. sync.Mutex is a prime example. A zero-valued mutex is unlocked. You can lock and unlock it immediately. new(sync.Mutex) gives you a pointer to a ready-to-use mutex.

package main

import (
    "fmt"
    "sync"
)

// Counter wraps an int with a mutex for safe concurrent access
type Counter struct {
    mu    sync.Mutex
    value int
}

func main() {
    // new() creates a pointer to a zeroed Counter.
    // The embedded sync.Mutex is also zeroed, which is its valid initial state.
    c := new(Counter)

    // Access requires locking. The mutex works immediately because zero-value is valid.
    c.mu.Lock()
    c.value++
    c.mu.Unlock()

    fmt.Println(c.value)
}
# output:
1

The Counter struct has a sync.Mutex and an int. new(Counter) zeroes both. The mutex is unlocked. The int is zero. The struct is ready. You don't need to call a constructor. You don't need to set fields. The zero value is the valid state.

This pattern is common in Go. Types like sync.WaitGroup, bytes.Buffer, and strings.Builder all work at zero value. new() is appropriate here. You allocate, and you use.

However, even for these types, composite literals are often preferred. &Counter{} is equivalent to new(Counter). It returns a pointer to a zeroed struct. The difference is syntax. &Counter{} makes it clear you're creating a struct. new(Counter) looks like a function call.

The Go community prefers composite literals for structs. &User{Name: "Alice"} is clearer than u := new(User); u.Name = "Alice". It shows intent and initialization in one step. gofmt aligns the fields in a composite literal, making it readable. new() doesn't get this treatment because it's just a function call.

Use new() when you truly just need zeroed memory. Use &T{} when you want to show you're creating a struct, even if you don't set fields. The convention is strong. Follow it.

Pitfalls and compiler errors

new() has a few traps. The most common is using it for collections. You've seen new([]int) leads to a nil slice. The same applies to maps and channels.

package main

func main() {
    // new() returns *map[string]int. The map is nil.
    m := new(map[string]int)
    // Dereference and assign.
    (*m)["key"] = 1
}

The program panics with panic: assignment to entry in nil map. The map header is zeroed. The internal hash table is nil. You must use make() for maps.

Another trap is shadowing the built-in. new is a built-in function, not a keyword. You can define a function named new in your package. This shadows the built-in.

package main

// new shadows the built-in new function. This is a footgun.
func new() {}

func main() {
    // The compiler rejects this with new is not a type because new is now a function, not the built-in
    _ = new(int)
}

The error is confusing. new is not a type suggests a syntax error, but the real issue is shadowing. The compiler sees new as your function, not the built-in. Your function takes no arguments, so new(int) is invalid.

Never shadow new. It breaks expectations and causes subtle bugs. The compiler allows it, but the community frowns on it. Stick to the built-in.

A third trap is using new() for strings. new(string) gives you a pointer to an empty string. You rarely need a pointer to a string. Strings are cheap to pass by value. They are immutable. Passing a pointer adds indirection without benefit.

Don't pass a *string. Strings are already cheap to pass by value. If you need a mutable string buffer, use strings.Builder. If you need a pointer for some reason, use &str. new(string) is almost never the right choice.

Decision matrix

Use new(T) when you need a pointer to a zero-valued type and the zero value is a valid, ready-to-use state. Use &T{} when you need a pointer to a struct and want to set fields immediately for readability. Use make() when you need to initialize slices, maps, or channels so their internal data structures are ready for use. Use new() sparingly; prefer composite literals for almost all struct allocation in application code.

new() allocates. make() initializes. Zero value is the constructor. Don't fight the type system.

Where to go next