6.4 new() vs &T{}: Two Ways to Allocate
Look, I get it. You’re staring at new(int) and &int{} and wondering if this is just another one of Go’s charmingly redundant features, like having two ways to declare a variable. Is it a coin toss? Absolutely not. While they can produce the same result in a simple case, they represent two fundamentally different philosophies of instantiation. One is a holdover, a blunt instrument; the other is the idiomatic, expressive way to bring a new struct into this cruel world.
Let’s cut through the noise. The new function is the old-school, C++-flavored way. It’s a built-in function that takes a type, allocates enough zeroed memory for it, and returns a pointer to that memory. It’s functional. It’s boring. It gets the job done, but it has the personality of a wet paper bag.
// Using new()
p := new(int) // p is of type *int, *p == 0
fmt.Println(*p) // Output: 0
s := new([]string) // s is of type *[]string, *s == nil
fmt.Println(*s) // Output: []
See that last one? new([]string) gives you a pointer to a slice, but the slice itself is nil. This is a classic foot-gun. You can’t append to a nil slice without first making it point to an actual array, which new doesn’t do for you. It just zeroes the memory, and for a slice, that means a nil value. It’s technically correct but practically useless in most scenarios.
The Case for Composite Literals (&T{})
This is the Go way. Instead of asking a function for a pointer to a zeroed type, you just describe the value you want and take its address directly. It’s declarative. You’re not asking for allocation; you’re stating what the allocated thing should be.
// Using a composite literal with &
p := &int(42) // p is *int, *p == 42. We initialized it this time!
fmt.Println(*p) // Output: 42
s := &[]string{} // s is *[]string, *s is an empty (non-nil) slice!
fmt.Println(*s) // Output: []
*s = append(*s, "hello")
fmt.Println(*s) // Output: [hello]
The key difference here is profound. &[]string{} creates an actual, non-nil, empty slice value (with a length and capacity of 0) and then gives us its address. This is immediately useful. You can append to it, range over it, and pass it to functions that expect a initialized slice without causing a panic. It does what you mean, not just what you say.
When new() Might Actually Be Useful (It’s Rare)
I’ve been ragging on new, but in the spirit of honesty, it has its microscopic niche. Its primary use is when you need a pointer variable but you want to defer the actual initialization of the value to later, perhaps in a different function. It’s a way to declare your intent: “I will have a pointer to a Foo here, but I’m not ready to construct the Foo itself yet.”
var p *MyComplexStruct
// This is a bit clunky. What is p? It's nil.
// Alternatively, with new:
p := new(MyComplexStruct)
// p is now a valid pointer to a zeroed MyComplexStruct
// Now I can pass it to a function that will populate it
initializeComplexStruct(p)
Even here, I’d often argue that returning a pointer from a constructor function is cleaner (p := NewComplexStruct()), but new can serve as a placeholder. It’s also very slightly cheaper in terms of CPU instructions for a zero-value allocation, but we’re talking nanoseconds. If that’s your bottleneck, you’re probably writing an OS kernel, not a web service.
The One Unavoidable Exception: No Generic new for Parameterized Types
Here’s a rough edge I told you I’d call out. Since Go’s new is a built-in function and not a generic function you can define yourself, it can’t handle parameterized types. This is where the composite literal syntax wins utterly.
type MyBox[T any] struct {
value T
}
// This works perfectly:
box := &MyBox[int]{value: 42}
// This is a syntax error. You can't instantiate 'new' with a type parameter.
// box := new(MyBox[int])
The language designers made a choice here. The built-in new is a primitive, and primitives don’t get upgraded for generics. The composite literal syntax, being part of the core expression grammar, just works. This alone should tell you which method is future-proof.
The Verdict: Use &T{}. Always. For structs, slices, maps, arrays—anything that isn’t a bare primitive. It’s more expressive, it avoids the nil-value foot-gun for reference types, it works with generics, and it’s what every other Gopher expects to see. Reserve new for the rare occasions where you genuinely need a pre-zeroed pointer to a primitive or a struct you’re about to pass to a dedicated initializer. But frankly, you’ll probably forget new exists, and your code will be better for it.