The new Built-in
How to use new in Go to allocate zeroed memory and obtain a pointer, and how it differs from make
Few built-in functions in Go cause as much early confusion as new. Its name suggests something essential for memory allocation, but its behavior is surprisingly narrow: it allocates a value of a given type, sets it to zero, and returns a pointer to it. That's it. The name carries weight from other languages, but in Go, new does not initialize maps, slices, or channels the way a newcomer might expect. Understanding exactly what new does—and what it does not do—clears up a large portion of pointer-related mistakes.
Allocating Zeroed Memory with new
The new function accepts a type as its sole argument and returns a pointer to a newly allocated zero value of that type.
p := new(int)
fmt.Println(*p) // 0
The variable p is of type *int. The memory it points to exists and is set to the zero value for int, which is 0. No further initialization happens—the value is ready but blank.
This works for any type. For a struct, every field becomes its zero value:
type User struct {
Name string
Age int
}
u := new(User)
fmt.Printf("%+v\n", *u) // {Name: Age:0}
The pointer u is not nil; it points to a valid, zeroed User. That means you can immediately assign fields without further ceremony:
u.Name = "Ali"
u.Age = 30
Safe to Use:
A value allocated with new is always safe to dereference—provided you do not reassign the pointer to nil. The memory exists and contains a valid zero value, so field assignment or reading works without panics.
The one-line mental model for beginners: new(T) is equivalent to &T{} when T is a struct, or to taking the address of a newly declared zero variable for basic types. There is no hidden initialization, no constructor, and no magic.
How new Works Under the Hood
At the language specification level, new(T) does two things: it allocates storage for a variable of type T and returns a value of type *T that points to it. The allocated value is the zero value of T.
What happens at runtime is more nuanced. The Go compiler performs escape analysis to decide whether the variable should live on the stack or the heap. A call to new does not guarantee a heap allocation. If the pointer does not outlive the function that creates it, the compiler often places the value on the stack, making the allocation nearly free.
func compute() int {
p := new(int)
*p = 42
return *p
}
Here p never leaves the function. The compiler can allocate *p on the stack because its lifetime is bounded by compute. No heap allocation occurs, and the garbage collector never sees it.
Contrast with a case where the pointer escapes:
var global *int
func escape() {
p := new(int)
*p = 99
global = p
}
Now the pointer is stored in a package-level variable, so the value must survive after escape returns. The compiler moves the allocation to the heap and the runtime allocator provides the memory.
Escape Analysis Decides:
Whether new causes a heap allocation depends entirely on how the returned pointer is used, not on the fact that new was called. The compiler is free to optimize allocations onto the stack whenever it can prove the pointer does not escape.
From the allocator's perspective, when heap allocation does happen, the runtime uses size classes and spans to allocate the memory efficiently. But from a programmer's viewpoint, you don't need to know those internals. The key takeaway is that new gives you a pointer; where the memory lives is the compiler's decision.
Common Mistakes with new
Using new with maps, slices, and channels
This is the most frequent misstep. Calling new on a map returns a pointer to a nil map value, which cannot be used for storage.
m := new(map[string]int)
(*m)["key"] = 42 // panic: assignment to entry in nil map
The same applies to slices and channels: new([]int) returns *[]int, pointing to a nil slice. Appending to *p works because append can handle nil slices, but that's an indirect and confusing pattern. Channels allocated with new(chan int) give a nil channel on which sends and receives block forever.
Never Use new for Maps, Slices, or Channels:
new only zeroes memory; it does not initialize the internal data structures of maps, slices, or channels. Use make for those types. A new(map[K]V) is a nil map—reading returns zero values, but writing panics.
Forgetting to dereference before use
When you allocate a struct with new, you get a pointer. Accessing a field requires either explicit dereferencing or relying on Go's automatic pointer dereferencing for field access.
u := new(User)
// These two lines are equivalent:
(*u).Name = "Ali"
u.Name = "Ali" // Go allows this shorthand
The shorthand u.Name works because the compiler knows u is a pointer to a struct. But for types like *int, you must dereference explicitly to read or write the value: *p = 10.
Expecting new to call a constructor
Go has no constructors. new(T) never runs custom initialization logic. If you need to set up a value with non-zero defaults or invariants, write a factory function that returns a pointer.
func NewUser(name string) *User {
return &User{Name: name, Age: 0}
}
Zero Values Can Hide Logic Errors:
A zeroed struct is valid in Go, but relying on a zero value for a field that should never be empty can introduce subtle bugs. If a User must always have a non-empty Name, a factory function is safer than new(User) because it can enforce that invariant.
The Difference Between new and make
Both new and make allocate memory, but they serve distinct purposes and return different types. Understanding the boundary prevents most pointer-related panics.
| Function | What it allocates | Return type | Applicable types |
|---|---|---|---|
new(T) | Zeroed value of type T | *T | Any type |
make(T, args...) | Initialized value of type T | T (not a pointer) | Only slice, map, channel |
The critical distinction lies in initialization. new fills memory with zeros and stops. make creates the underlying data structure: for a slice, it allocates a backing array and sets length and capacity; for a map, it allocates a hash table ready for keys; for a channel, it creates a communication buffer.
Consider this side-by-side:
// Using new on a slice — returns *[]int, pointing to nil slice
sp := new([]int)
fmt.Println(*sp == nil) // true
// You can append to *sp, but it's an odd pattern.
// Using make on a slice — returns []int with a backing array
s := make([]int, 0, 5)
s = append(s, 1, 2, 3)
fmt.Println(s) // [1 2 3]
A simple rule: if you are working with a slice, map, or channel, use make. For everything else, if you need a pointer to a zero value, new works.
Why Two Keywords?:
The split between new and make exists because maps, slices, and channels need runtime-specific initialization that zeroing alone cannot provide. new returns a pointer to a zeroed value, but for these reference types, that pointer is nearly useless without the internal structures that make sets up. The language separates the two operations to make this distinction explicit.
A common point of confusion: make does not return a pointer. A slice value itself is a small struct containing a pointer to the backing array, a length, and a capacity. You pass that slice value around, and modifications to its elements are visible through the shared backing array. You don't need an extra *[]int unless you intend to change the slice header itself (length, capacity, or backing array) inside a function.
When to Use new in Practice
In typical Go code, direct calls to new are less frequent than you might expect. Composite literals with & often serve the same purpose more readably.
// Equivalent:
u1 := new(User)
u2 := &User{}
The second form is more common because it can also include field initialization inline:
admin := &User{Name: "Admin", Age: 30}
So when does new still appear? Primarily in generic code where the type parameter is not a struct with named fields, or when you need a pointer to a basic type without declaring a variable:
func PointerTo[T any](v T) *T {
p := new(T)
*p = v
return p
}
Here new(T) allocates a zero value of whatever type T is, and the function overwrites it with v. This is safe because new(T) works for any type, while &T{} is only valid for struct types.
Another scenario: when a function expects a pointer to an integer or a boolean and you want to pass a zero value without declaring a temporary variable.
func configure(debug *bool) {
if debug != nil && *debug {
fmt.Println("Debug mode on")
}
}
configure(new(bool)) // passes a pointer to false
In production code, this pattern appears in configuration structs or optional parameters where the caller needs to supply a boolean flag without creating a named variable.
Avoid new for Optional Fields with Defaults:
Using new(bool) to pass a pointer to false can be confusing when the receiving function treats nil differently from a pointer to false. In such cases, consider using a helper function like func boolPtr(b bool) *bool { return &b } that makes the intent explicit: configure(boolPtr(false)).
Summary
The new built-in occupies a small but precise role in Go's memory model. It allocates a zeroed value and hands back a pointer. That simplicity is its strength: no hidden initialization, no heap-vs-stack promises, just a raw, blank value ready to be used.
The real lesson to carry forward is not about new itself, but about the sharp line between zeroed memory and initialized structures. That line is what separates new from make—and knowing which side you're on prevents the vast majority of pointer and allocation bugs. If you find yourself reaching for new on a map, slice, or channel, pause and ask whether you really meant make.