Golang Memory Management: Stack vs Heap

Golang uses two types of memory: stack and heap.

Stack

Heap

Memory Allocation: new vs make

Use Case 1: Stack Program Without Pointers

package main

func main() {
        n := 4
        n2 := square(n)
        println(n2)
}

func square(n int) int {
        return n * n
}

func println(_ int) {
}

There will be:

Once the square calculation is done, Go does not clear the stack frame. The value of n is still part of the square stack frame. It tracks valid and invalid stack frames, marking the square frame as invalid.

Then, the println stack frame replaces the square stack frame, like a self-cleaning process.

Use Case 2: Stack Program with Pointers

package main

func main() {
    n := 4
    square(&n)
    println(n)
}

func square(n *int) int {
    return *n * *n
}

func println(_ int) {
}

Escape analysis output:

go build -gcflags="-m -l" test.go
# command-line-arguments
./test.go:9:13: n does not escape

In this case, two stack frames will be created:

The square function has a pointer n which points to the main stack frame’s value of n. When using pointers, it stays on the stack itself. This is called “passing pointer down on function typically stays on the stack”.

Use Case 3: Returning Pointer

package main

func main() {
    n := 4
    n1 := square(&n)
    println(*n1)
}

func square(x *int) *int {
    y := *x * *x
    return &y
}

func println(_ int) {
}

Escape analysis output:

go build -gcflags="-m -l" test.go
# command-line-arguments
./test.go:9:13: x does not escape
./test.go:10:2: moved to heap: y

When the square function returns a pointer, it returns a pointer from the square stack frame, which becomes invalid as per stack cleaning. Then how does println access n1? The Go compiler understands that y cannot be part of the stack frame once square returns, so y will be part of the heap. This is called “sharing up (returning pointer) typically escapes to heap.”

Deep Dive into Memory Allocation

Only the compiler knows whether memory will be on the heap or stack. Below are a few reasons why values are constructed on the heap instead of the stack:

Example: IO Reader Interface

This explains why the IO reader is written this way:

type Reader interface {
    Read(b []byte) (n int, err error)
}

Instead of:

type Reader interface {
    Read(n int) (b []byte, err error)
}

With the second approach, there would be many heap allocations, which would be inefficient.

References

Understanding Allocations: the Stack and the Heap - GopherCon SG 2019