Question

Stack vs Heap Allocation in Go: Structs, Escape Analysis, and Garbage Collection

goheap-memorystack-memory

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
  • malloc allocates 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

These are not strict rules in Go:

  • new(T) does not automatically mean heap
  • var x T does not automatically mean stack

The compiler decides based on usage.

Why this matters

This design gives Go two advantages:

  1. Safety: returning the address of a local variable is valid when needed.
  2. Performance: values that do not escape can stay on the stack, which is faster and creates less garbage for the GC.

Garbage collection and lifetimes

The garbage collector is responsible for reclaiming heap memory that is no longer reachable. Stack memory is not GC-managed in the same way; it is naturally released when the function call ends.

So the lifetime model is:

  • if a value stays within the function and does not escape, stack allocation is often possible
  • if a value must live longer, the compiler arranges heap allocation
  • the GC later frees heap memory when nothing references it anymore

Why structs are still passed by value

Even if a struct happens to be heap-allocated, the language semantics are still value-based.

That means:

func f(s MyStructType)

receives a copy of the struct value.

Where the bytes physically live in memory is a separate implementation detail from how assignment and function calls behave at the language level.

Pointers still matter because they let you:

  • avoid copying large structs
  • modify the original value
  • represent optional values (nil)
  • share access to the same data across functions or objects

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.

Take Quiz

Syntax and Examples

Core syntax

Allocate a zero-valued struct and get a pointer

p := new(MyStructType)
  • p has 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
  • v has type MyStructType
  • p has type *MyStructType
  • if p escapes, v may be heap-allocated

Composite literal with address

p := &MyStructType{}

This is a common short form.

Example: both forms are valid

package main

import "fmt"

type User struct {
	Name string
	Age  int
}

func makeUserWithNew() *User {
	u := new(User)
	u.Name = "Ava"
	u.Age = 30
	return u
}

func makeUserWithLocal() *User {
	var u User
	u.Name = "Ava"
	u.Age = 30
	return &u
}

func main() {
	a := makeUserWithNew()
	b := makeUserWithLocal()

	fmt.Println(a.Name, a.Age)
	fmt.Println(b.Name, b.Age)
}

Both functions work correctly.

Example: passing by value vs pointer

package main

import "fmt"

type Counter struct {
	Value int
}

func incrementByValue(c Counter) {
	c.Value++
}

func incrementByPointer(c *Counter) {
	c.Value++
}

func main() {
	c := Counter{Value: 10}

	incrementByValue(c)
	fmt.Println(c.Value) // 10

	incrementByPointer(&c)
	fmt.Println(c.Value) // 11
}

What this shows

  • Passing c copies the struct.
  • Passing &c shares access to the original struct.
  • This is true regardless of whether the underlying storage is stack or heap.

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

  1. var p Point

    • A Point value is created.
    • At this moment, you should think of it as a normal local value.
  2. p.X = 4

    • The X field is set to 4.
  3. p.Y = 7

    • The Y field is set to 7.
  4. return &p

    • You return the address of p.
    • The compiler sees that code outside the function will still need access to p.
    • Therefore p escapes the function.
  5. Compiler decision

    • Because p escapes, the compiler allocates it so it survives after the function returns.
    • In practice, this usually means heap allocation.
  6. Function returns

    • The returned pointer still points to valid memory.

Compare with a non-escaping value

func sum() int {
	var a int = 2
	var b int = 3
	return a + b
}

Here, a and b do not escape. They are usually good candidates for stack allocation.

How to inspect escape behavior

You can ask the Go compiler to show escape analysis information:

go build -gcflags="-m" ./...

You may see messages like:

moved to heap: p

That tells you the compiler decided the value escapes.

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(id int) *User {
	// return nil if not found
	return nil
}

A plain struct value cannot be nil, but a pointer can.

Methods that modify receiver state

Pointer receivers let methods update the original struct:

func (c *Counter) Increment() {
	c.Value++
}
Take Quiz

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 nil, fmt.Errorf("name is required")
	}
	return &User{Name: name}, nil
}

This combines:

  • guard clauses
  • explicit error handling
  • pointer returns for optional success values

Avoiding unnecessary escapes in hot code

In performance-critical code, developers sometimes avoid patterns that cause values to escape unnecessarily, because heap allocations can:

  • increase GC work
  • reduce throughput
  • add latency

For example, a short-lived helper object used only inside a function may be kept as a local value rather than returned or stored elsewhere.

Rule of thumb in real projects

  • Choose value types when copying is cheap and ownership should be clear.
  • Choose pointers when mutation, sharing, nil, or reduced copying is important.
  • Let the compiler decide stack vs heap unless profiling shows a real problem.

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.

package main

type Data struct {
	N int
}

func main() {
	a := Data{N: 1}
	b := a
	b.N = 99
	_ = a // a.N is still 1
}

Avoid it

Separate these ideas:

  • where memory lives = implementation detail
  • copy vs share behavior = language semantics

Mistake 4: Using pointers everywhere

Broken design example:

func add(a *int, b *int) *int {
	result := *a + *b
	return &result
}

This works, but it is awkward and unnecessary for simple integers.

Better

func add(a, b int) int {
	return a + b
}

Avoid it

Use pointers when they provide a real benefit:

  • mutation
  • shared access
  • nilability
  • avoiding expensive copies

Mistake 5: Forgetting that pointer receivers can mutate shared state

func (c *Config) SetPort(p int) {
	c.Port = p
}

If several parts of the program share the same pointer, one change affects all of them.

Avoid it

Use value copies when you want isolation.

Mistake 6: Optimizing allocation style too early

Beginners sometimes rewrite code just to “keep things on the stack.”

Avoid it

Prefer:

  • readable code first
  • measure with benchmarks and profiling
  • inspect escapes only when performance matters

Comparisons

ConceptValue TypePointer Type
MeaningThe actual dataAddress of the data
Function callCopies the valueCopies the pointer
Can be nilNoYes
Can modify original through itNoYes
Good for small structsYesSometimes unnecessary
Good for large/mutable/shared structsSometimes costlyOften useful

new(T) vs &T{}

SyntaxResultTypical use
new(T)*T to zero valueSimple zero-initialized allocation
&T{}*T to initialized valueMore common when setting fields

Example:

u1 := new(User)
u2 := &User{}

Both produce *User.

Stack vs heap in Go vs C

LanguageLocal variableReturning address of local
CUsually stackInvalid
GoCompiler decidesValid if value escapes

Passing by value vs passing a pointer

FormExampleEffect
By valuef(s)s is copied
By pointerf(&s)function can access original

The important distinction is not stack vs heap, but copy vs shared access.

Take Quiz

Cheat Sheet

Quick rules

  • var x T creates a value of type T
  • new(T) returns *T
  • &x gives a pointer to x
  • &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

When pointers often make sense

  • need to modify original data
  • want to avoid copying large structs
  • need nil to mean “missing”
  • methods use pointer receivers

Useful compiler command

go build -gcflags="-m" ./...

Look for messages such as:

moved to heap: x

Important edge case

A variable being declared locally does not mean it must be on the stack. If it escapes, the compiler can place it on the heap.

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?

Sometimes, yes. For small structs that do not need mutation or nil, returning a value is often simple and clear.

Take Quiz

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

  1. Define a User struct with at least Name and Score fields.
  2. Write one function that returns a User by value.
  3. Write one function that returns *User by pointer.
  4. Write update functions that try to modify the user using both value and pointer parameters.
  5. Print results to show which updates affect the original data.
Take Quiz

Keep learning

Related questions

View more