Go

How do goroutine stacks grow, and why does that constrain the language?

Question 334HardGo 1.22 to 1.25

A goroutine starts with a small stack, historically 2 KB. Since Go 1.19 the runtime picks the starting size from the average stack usage it has observed. Every function that is not a leaf has a prologue that compares the stack pointer with a guard value. If there is not enough room, it calls runtime.morestack, which allocates a stack twice as large, copies the whole stack into it, and adjusts every pointer into the stack. Go has used these contiguous, copying stacks since 1.4, replacing segmented stacks and their "hot split" problem. During GC, a stack using less than a quarter of its space is shrunk.

Consequences:

  • A heap object must never point into a stack, because stacks move. The compiler enforces this through escape analysis.
  • Addresses of stack variables are not stable, which is why you must not keep them as uintptr.
var sink byte

//go:noinline
func grow(n int) {
    var pad [1024]byte // must really be used, or the compiler drops it
    pad[n%len(pad)] = byte(n)
    if n > 0 {
        grow(n - 1)
    }
    sink += pad[n%len(pad)] // keeps pad live across the recursive call
}

func main() {
    x := 1
    before := uintptr(unsafe.Pointer(&x))
    grow(200) // ~200 KB of frames: forces several stack copies
    after := uintptr(unsafe.Pointer(&x))
    fmt.Println(before == after) // typically false: x moved with the stack
}

Goroutines are cheap because they start small, and deep recursion is fine up to the maximum stack size, 1 GB on 64-bit, beyond which you get a fatal "stack overflow".

More on Memory, GC & Runtime Internals

All 38 Memory, GC & Runtime Internals questions