Question
Stack vs Heap Allocation in Go: Structs, Escape Analysis, and Garbage Collection
Question
In Go, I am trying to understand how struct allocation works compared with C and Python.
These two functions appear to behave the same way:
func myFunction() (*MyStructType, error) {
var chunk *MyStructType = new(MyStructType)
// ...
return chunk, nil
}
func myFunction() (*MyStructType, error) {
var chunk MyStructType
// ...
return &chunk, nil
}
Both seem to create a struct and return a pointer to it.
In C, these would be very different:
- The first would allocate on the heap.
- The second would create a local stack variable.
- Returning the address of that local variable would be invalid after the function returns.
In Python-style languages, local names usually refer to heap-allocated objects, so the second form would not typically exist in the same way.
I understand that Go allows returning the address of a local variable, and that this is safe because the value can outlive the function call.
This leads to a few questions:
- In the first example, the struct is heap-allocated. What happens in the second example? Is it created on the stack or on the heap?
- If the second example starts on the stack, how can it remain valid after the function returns?
- If it is heap-allocated, why are structs still passed by value in Go rather than always by reference? What role do pointers still play?
Short Answer
By the end of this page, you will understand that Go does not treat var x T and new(T) as fixed promises about stack or heap placement. Instead, the compiler decides where values live using escape analysis. If a value must outlive the current function, Go allocates it so it remains valid, usually on the heap. You will also see why Go still has value semantics, why pointers are useful, and how garbage collection fits into the picture.
Concept
In Go, stack vs heap allocation is mostly a compiler decision, not something you directly control by syntax alone.
In languages like C, the storage duration of a variable is closely tied to how and where it is declared:
- local variables usually live on the stack
mallocallocates on the heap- returning a pointer to a local variable is invalid
Go is different. Go has:
- value types, including structs
- pointers to values
- garbage collection for memory that is no longer reachable
- escape analysis, which decides whether a value can safely stay on the stack or must be moved to the heap
The key rule
If a value's address is used in a way that may outlive the current function call, that value escapes. When that happens, the compiler typically allocates it on the heap.
So in these two forms:
chunk := new(MyStructType)
return chunk
and
var chunk MyStructType
return &chunk
both can end up producing equivalent behavior. In the second case, the compiler sees that chunk is returned by address, so it must remain valid after the function returns. Therefore, it allocates storage in a way that survives the function call.
Important idea: syntax does not guarantee stack or heap
Mental Model
Think of Go values as packages, and the compiler as the shipping manager.
- A value is the package itself.
- A pointer is the delivery address to that package.
- The stack is a temporary worktable.
- The heap is long-term storage.
If a package is only needed during one small task, the manager keeps it on the worktable.
If someone needs to keep using that package after the current task finishes, the manager moves it to long-term storage and gives out its address.
So when you write:
var chunk MyStructType
return &chunk
it is like saying: “I need to give someone the address of this package after I leave the room.”
The compiler notices that and says: “Then this cannot stay on the temporary table. I will store it somewhere that remains valid.”
The important part is that you still created a value, not a magical reference object. A pointer is just a way to point to that value.
Syntax and Examples
Core syntax
Allocate a zero-valued struct and get a pointer
p := new(MyStructType)
phas type*MyStructType- the struct fields are zero-initialized
- allocation may be on stack or heap depending on escape analysis
Create a value, then take its address
var v MyStructType
p := &v
vhas typeMyStructTypephas type*MyStructType- if
pescapes,vmay be heap-allocated
Composite literal with address
p := &MyStructType{}
This is a common short form.
Example: both forms are valid
package main
import "fmt"
User {
Name
Age
}
*User {
u := (User)
u.Name =
u.Age =
u
}
*User {
u User
u.Name =
u.Age =
&u
}
{
a := makeUserWithNew()
b := makeUserWithLocal()
fmt.Println(a.Name, a.Age)
fmt.Println(b.Name, b.Age)
}
Step by Step Execution
Consider this function:
func buildPoint() *Point {
var p Point
p.X = 4
p.Y = 7
return &p
}
with:
type Point struct {
X int
Y int
}
Step-by-step
-
var p Point- A
Pointvalue is created. - At this moment, you should think of it as a normal local value.
- A
-
p.X = 4- The
Xfield is set to4.
- The
-
p.Y = 7- The
Yfield is set to7.
- The
-
return &p
Real World Use Cases
Returning objects from constructor-like functions
A very common pattern in Go is returning a pointer from a factory function:
func NewConfig() *Config {
return &Config{Port: 8080}
}
This is used for:
- configuration objects
- service instances
- database wrappers
- HTTP handlers
Building response objects in APIs
When preparing a response struct in a helper function, you may return a pointer so callers can mutate it or avoid copying:
func BuildUserResponse(name string) *UserResponse {
return &UserResponse{Name: name}
}
Large structs in data processing
If a struct is large, passing pointers can reduce copying costs.
Examples:
- parsed records
- image metadata
- request context objects
- cached entities
Optional data with nil
Pointers are often used when a value may be absent:
func FindUser *User {
}
Real Codebase Usage
In real Go codebases, developers usually focus less on “forcing stack or heap” and more on writing clear code while being aware of escape costs in performance-sensitive paths.
Common patterns
Constructor functions returning pointers
func NewServer(addr string) *Server {
return &Server{Addr: addr}
}
This is common when:
- the struct has methods with pointer receivers
- the struct is large
- the struct contains mutable state
Returning values for small immutable-like structs
func NewPoint(x, y int) Point {
return Point{X: x, Y: y}
}
This is common when:
- the struct is small
- copying is cheap
- value semantics are preferred
Validation before returning pointers
func NewUser(name string) (*User, error) {
if name == "" {
return , fmt.Errorf()
}
&User{Name: name},
}
Common Mistakes
Mistake 1: Assuming new(T) always means heap allocation
p := new(MyStructType)
Beginners often think this always allocates on the heap. In Go, it may or may not. The compiler decides.
Avoid it
Think of new(T) as:
- create a zero value of type
T - return
*T
Do not treat it as a direct heap command like malloc.
Mistake 2: Assuming local variables always live on the stack
func f() *MyStructType {
var x MyStructType
return &x
}
In C, this would be invalid. In Go, it is fine because x escapes.
Avoid it
Remember: local declaration does not guarantee stack allocation.
Mistake 3: Confusing location with semantics
A struct may be heap-allocated, but assigning it still copies the value.
Comparisons
| Concept | Value Type | Pointer Type |
|---|---|---|
| Meaning | The actual data | Address of the data |
| Function call | Copies the value | Copies the pointer |
Can be nil | No | Yes |
| Can modify original through it | No | Yes |
| Good for small structs | Yes | Sometimes unnecessary |
| Good for large/mutable/shared structs | Sometimes costly | Often useful |
new(T) vs &T{}
| Syntax |
|---|
Cheat Sheet
Quick rules
var x Tcreates a value of typeTnew(T)returns*T&xgives a pointer tox&T{...}creates a pointer to a struct literal- Go uses escape analysis to choose stack or heap
- Returning the address of a local variable is safe in Go
- Structs are still passed by value unless you use pointers explicitly
Common forms
var s MyStructType // value
p := &s // pointer to value
p2 := new(MyStructType) // pointer to zero value
p3 := &MyStructType{} // pointer to struct literal
Mental rules
- Syntax does not guarantee allocation location
- Heap allocation is about lifetime
- Value semantics are about copying behavior
- Pointers are about sharing and mutation
When values often make sense
- small structs
- immutable-style usage
- clearer ownership
- no need for
nil
FAQ
Is new() the same as heap allocation in Go?
No. new(T) creates a pointer to a zero value of type T, but the compiler still decides whether the storage goes on the stack or heap.
Why is returning &localVar safe in Go?
Because if the compiler sees that the local variable escapes the function, it allocates it so the value remains valid after return.
Are structs in Go passed by value or by reference?
Structs are passed by value. If you want shared access or mutation, pass a pointer.
If something is heap-allocated, does that mean it is passed by reference?
No. Allocation location and passing semantics are different concepts. A heap-allocated struct can still be copied as a value.
When should I use a pointer to a struct in Go?
Use a pointer when you need to modify the original, avoid copying a large struct, support nil, or share state.
Does garbage collection manage stack memory too?
Not in the same way. Stack frames are cleaned up automatically when functions return. Garbage collection is mainly concerned with unreachable heap objects.
How can I tell whether a variable escapes to the heap?
Use the compiler's escape analysis output:
go build -gcflags="-m"
Is returning a struct value instead of a pointer more idiomatic?
Mini Project
Description
Build a small Go program that creates and updates user profiles using both value returns and pointer returns. This project demonstrates how Go handles structs, copying, mutation, and returning addresses of local variables safely.
Goal
Create a program that shows the practical difference between returning a struct by value and returning a pointer to a struct.
Requirements
- Define a
Userstruct with at leastNameandScorefields. - Write one function that returns a
Userby value. - Write one function that returns
*Userby pointer. - Write update functions that try to modify the user using both value and pointer parameters.
- Print results to show which updates affect the original data.
Keep learning
Related questions
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.
Blank Identifier Imports in Go: What `_` Means in an Import Statement
Learn what `_` means in a Go import, why blank identifier imports run package init code, and when to use them safely.
Calling Functions Across Files in the Same Go Package
Learn how Go uses packages across multiple files, why functions may appear undefined, and how to organize code correctly.