Stack Growth and Copying

Updated

September 8, 2026

Stack Growth and Copying

Overview

Each goroutine starts with a small stack (on the order of a few KB historically; exact size is version-dependent) and grows by allocating a larger stack and copying live frames when needed. Stacks also shrink when underutilized.

This design makes millions of Gs feasible — you do not pre-reserve 1–8 MiB per goroutine like many OS threads.

Diagram: Stack growth

  G stack nearly full
       │
       v
  morestack → larger stack
       │
       v
  copy frames + fix pointers → resume

Growth Path

G running, stack nearly full
  -> morestack
  -> allocate larger stack
  -> copy stack contents
  -> adjust pointers into stack (compiler/runtime)
  -> resume

Go forbids taking the address of a stack variable and using it after the frame dies; the compiler and escape analysis keep GC and stack copy correct.

Why Deep Recursion Hurts

Deep recursion causes repeated growth and large stacks:

func sum(n int) int {
    if n == 0 {
        return 0
    }
    return n + sum(n-1) // grows stack with depth
}

Prefer iterative algorithms for unbounded depth.

cgo and Stacks

cgo may switch to larger system stacks for C execution. Crossing Go↔︎C frequently is expensive (scheduler + stack). See cgo transitions.

Debugging Stacks

GOTRACEBACK=all ./app
// panic dumps all Gs
buf := make([]byte, 1<<16)
n := runtime.Stack(buf, true) // all goroutines if true
fmt.Printf("%s", buf[:n])

Nosplit

//go:nosplit
func tight() { /* cannot grow stack; for runtime/asm constraints */ }

User code almost never needs //go:nosplit. Misuse panics if the frame does not fit.

Experiment

go mod init example
go run .
package main

import (
    "fmt"
    "runtime"
)

func depth(n int, max int) int {
    if n == max {
        var st runtime.StackRecord
        // approximate: print goroutine stack size indirectly via Stack
        buf := make([]byte, 64*1024)
        k := runtime.Stack(buf, false)
        fmt.Printf("at depth %d stack dump bytes=%d\n", n, k)
        return n
    }
    return depth(n+1, max)
}

func main() {
    fmt.Println("GOMAXPROCS", runtime.GOMAXPROCS(0))
    _ = depth(0, 200)
}

What to notice: Deep call chains produce large stack dumps; production code should bound recursion and prefer workers with small frames.

Try next: Compare a recursive tree walk vs explicit stack slice for a large tree — RSS and latency under load.