Introduction: Why "just a struct" isn't that simple?
\n\nHave you ever written code in Go and, looking at your program, asked yourself: "Where does my struct live right now? On the fast and cozy stack or in the vast expanses of the heap under the supervision of the garbage collector?" This question plagues not only beginners. On Stack Overflow, the topic "Stack vs heap allocation of structs in Go, and how they relate to garbage collection" has gathered thousands of views. And this is no coincidence.
\n\nThe apparent simplicity of Go hides a powerful mechanism — escape analysis. It's a compiler detective that decides the fate of every object. Its decision affects your application's performance and the load on the GC (Garbage Collector). In this step-by-step tutorial, we'll put on our detective hat and figure out:
\n- \n
- Why does one struct live on the stack while another lives on the heap? \n
- How do pointers and closures break the stack's idyll. \n
- How this knowledge will help you write fast and efficient code in Go. \n
Stack vs Heap: A Quick Primer
\nBefore diving into the code, let's refresh the theory. Don't worry, it'll be quick and painless.
\n\nStack
\n- \n
- Speed: Lightning fast. Memory allocation is simply moving a pointer down. \n
- Lifecycle: Strictly tied to a function call. Lives as long as the function is running. \n
- Management: Automatic. "Burns up" when the function exits. \n
- Size: Small and fixed per goroutine (usually 2-4 KB). \n
Heap
\n- \n
- Speed: Slower. Requires searching for a free memory block. \n
- Lifecycle: As long as there are references to the object from the program. Can outlive the function that created it. \n
- Management: Garbage Collector (GC). It scans the heap, finds "dead" objects, and frees memory. This costs CPU resources. \n
- Size: Large (limited only by OS memory). \n
The golden rule of Go: "Keep data on the stack as long as possible". This reduces the load on the GC and speeds up the program.
\n\nEscape Analysis: Go's Main Detective
\nIn Go, you don't write new or malloc to explicitly specify where to place an object. The compiler decides this for you. This process is called escape analysis.
It looks at your code and asks one question: "Can a reference to this object leave the current function?".
\n- \n
- If NO (the object does not escape) → The object is placed on the stack. \n
- If YES (the object escapes) → The object is placed on the heap. \n
Let's see this in action.
\n\nExample 1: Safe struct on the stack
\npackage main\n\nimport "fmt"\n\ntype Point struct {\n X, Y int\n}\n\n// createPoint creates a Point and returns it by value.\nfunc createPoint(x, y int) Point {\n p := Point{X: x, Y: y}\n return p // p is copied on return\n}\n\nfunc main() {\n p1 := createPoint(10, 20)\n fmt.Println(p1)\n}\n\nIn this example, p is a local variable. It is created inside createPoint and returned by value (copied). The struct p itself cannot be used outside the function (its copy is used in main). Escape analysis sees this and happily places p on the stack. The GC rests.
Example 2: Escape via a pointer
\nNow let's change just one line — return a pointer.
\npackage main\n\nimport "fmt"\n\ntype Point struct {\n X, Y int\n}\n\n// createPointer creates a Point and returns a pointer to it.\nfunc createPointer(x, y int) *Point {\n p := P