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 typeTand returns*T.make(T, ...)creates and initializes certain built-in reference-like types and returns a value of typeT, 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 valuemake()→ 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 storagemake()initializes special built-in data structures- returning locals is safe because Go handles the memory details for you
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()
-
n := 7- A local integer variable is created with value
7.
- A local integer variable is created with value
-
m := make(map[string]int)- A usable empty map is created.
- This map is initialized and ready to store key-value pairs.
-
m["value"] = n- The map now contains one entry:
"value" -> 7.
- The map now contains one entry:
-
return &n, m- The function returns a pointer to
nand the mapm. - This is safe, even though
nis local.
- The function returns a pointer to
Inside main()
-
p, m := build()preceives the pointer to the integer.mreceives the map.
-
fmt.Println(*p)- Dereferences the pointer and prints
7.
- Dereferences the pointer and prints
-
fmt.Println(m["value"])- Looks up
"value"in the map and prints7.
- Looks up
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.
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()
| Feature | new() | make() |
|---|---|---|
| Works with | Any type | Only slice, map, channel |
| Returns | Pointer to type (*T) | The type itself (T) |
| Initializes internal runtime data | No | Yes |
| Common use | Get pointer to zero value | Create usable slice/map/channel |
new(User) vs &User{}
| Form | Meaning | Common use |
|---|---|---|
new(User) | Pointer to zero-value User | When you want all zero values |
&User{} | Pointer to a new User literal | Very common and idiomatic |
&User{Name: "Ava"} | Pointer with fields set | Best when initializing fields |
Nil map vs made map
| Form | Can read? | Can write? |
|---|---|---|
var m map[string]int | Yes, returns zero value | No, panics |
make(map[string]int) | Yes | Yes |
Nil slice vs made slice
| Form | Length | Can append? |
|---|---|---|
var s []int | 0 | Yes |
make([]int, 0) | 0 | Yes |
make([]int, 3) | 3 | Yes |
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*Tmake(T, ...)returnsTmake()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.
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
- Create a function that accepts a slice of words.
- Use a map to count how many times each word appears.
- Initialize the map correctly so assignments do not panic.
- Return the map from the function.
- In
main(), call the function and print the results.
Keep learning
Related questions
Are Go Maps Passed by Value or Reference?
Learn Go map parameter and return semantics: maps are passed by value, while copies share underlying data. Know when map pointers are useful.
Authentication for Go REST APIs: Sessions, Tokens, and Middleware
Learn practical authentication patterns for Go REST APIs, including password hashing, server sessions, bearer tokens, middleware, and security rules.
Automatic Build Versioning in Go: Embed Incrementing Build Numbers
Learn how to add automatic build versioning in Go using linker flags, build metadata, CI counters, and Git-based version values.