DEV Community

Cover image for Closures in Go:- escape analysis, memory layout, and runtime behavior
Saiful Islam
Saiful Islam

Posted on

Closures in Go:- escape analysis, memory layout, and runtime behavior


Closures in Go Includes the exact code, what the compiler does at compile-time, runtime steps, stack vs heap comparison, a compact memory diagram, and GC is here.


Table of contents

  1. Starting with a code Example
  2. Program output
  3. What is a closure
  4. Why closures matter
  5. Compiler time: code/data segments & compilation flow
  6. Key segments (table)
  7. Escape analysis — what it is and how it decides
  8. Runtime behavior — stepwise
    • Program startup
    • Call to outer()
    • Invoking the closure
    • Multiple closures
    • End of life
  9. Stack vs Heap — compact comparison table
  10. Memory visualization (CLI)
  11. Garbage collector role

12. Minimal step-by-step trace (first outer() and two closure calls)

1. Starting with a code Example

package main

import "fmt"

const a = 10    // compile-time constant
var p = 100     // package-level variable

func init() {
    fmt.Println("Bangla Bank")
}

func outer() func() {
    age := 30
    money := 100

    fmt.Printf("Age = %d\n", age)

    show := func() {
        money = money + a + p
        fmt.Println(money)
    }

    return show
}

func main() {
    incr1 := outer() // creates closure capturing `money`
    incr1()          // prints updated money
    incr1()          // prints updated money again

    incr2 := outer() // new closure instance — independent `money`
    incr2()
    incr2()
}
Enter fullscreen mode Exit fullscreen mode

2. Program output

Bangla Bank
Age = 30
210
320
Age = 30
210
320
Enter fullscreen mode Exit fullscreen mode

Note: two separate closures created by two calls to outer() (incr1 and incr2), each with its own money value so their state does not interfere.


3. What is a closure

A closure in Go is a function value (commonly an anonymous function) that captures identifiers (variables) from its surrounding lexical scope. Captured variables remain accessible to the closure after the outer function returns. In the example above, show captures money. Because show is returned and can be invoked later, the captured money must survive the lifetime of outer().

Implementation note (runtime view): a function value is usually represented as a small runtime struct containing a pointer to the compiled code and a pointer to the closure's environment (the captured variables).


4. Why closures matter

  • Closures allow a function to carry state with it (function + environment).
  • Each call to outer() returns an independent function value with its own captured environment. That’s why incr1() and incr2() maintain separate money values.
  • Knowing how captured variables are stored (stack vs heap) is essential for reasoning about performance and lifetime.

5. Compiler time: code/data segments & compilation flow

At compile-time the Go toolchain typically:

  • Compiles function bodies into machine code → code segment (read-only).
  • Places package-level variables into the data segment (globals).
  • Treats local variables as stack candidates until escape analysis decides they must escape.
  • Produces a binary where constants, functions, and globals are laid into appropriate areas.

The escape analysis pass marks variables that must live beyond their creating function; the compiler will then arrange for those variables to be heap-allocated.


6. Key segments and examples from the program

Segment What it stores (this program)
Code segment compiled instructions for init, main, outer, and the anonymous show function. (Note: const a is a compile-time constant — see "Anomalies & fine points" for nuance.)
Data segment package/global vars: p
Stack local variables used only during function execution (e.g., age)
Heap variables that escape (e.g., the money cell when captured by the closure)

7. Escape analysis- what it is and how it decides

Escape analysis is a compile-time analysis that decides whether a variable can safely live on the stack or must be moved (escape) to the heap.

Simplified rules (high level):

  • If a variable is used only inside the function and cannot be referenced afterwards → keep it on the stack.
  • If a variable's address is taken, stored in a place that outlives the function, or is captured by a function value that may outlive the function (returned or stored elsewhere) → the variable escapes and is allocated on the heap.

In the example:

  • age is used only inside outer() and is never referenced by show → stays on the stack.
  • money is referenced by show and show is returned → money escapes and is allocated on the heap.

8. Runtime behavior- function values, stack frames, heap objects

Program startup

  • Package init() runs (prints Bangla Bank).
  • Program loads code/data segments and prepares to execute main().

Call to outer()

  • A stack frame for outer() is created.
  • age := 30 is allocated as a stack-local candidate.
  • money := 100 is created but marked by escape analysis as escaping; the compiler arranges for it to be allocated on the heap (addressable by the closure environment).
  • The anonymous function show has compiled code in the code segment; the runtime builds a function value that contains:
    • a pointer to the compiled code for the anonymous function, and
    • a pointer to the captured environment (a heap object holding money).
  • outer() returns that function value; the stack frame is popped but the heap environment remains reachable through the function value.

Invoking the closure

  • Calling the function value (e.g., incr1()) jumps to the code pointed to by the function value.
  • The closure code uses the environment pointer to access and modify the heap-stored money cell.
  • Repeated calls mutate the same heap cell referenced by the closure (so state persists across calls).

Multiple closures

  • Each call to outer() that returns a closure creates a separate heap allocation for money.
  • incr1 and incr2 therefore refer to different heap objects; their money states do not interfere.

End of life

  • When there are no remaining references to a closure's function value (and thus to its environment), the heap object becomes unreachable and the GC may reclaim it.

9. Stack vs Heap- compact comparison table

Property Stack Heap
Allocation Automatic per function call By compiler/runtime when variable escapes
Lifetime LIFO — ends when function returns Lives until GC collects unreachable objects
Use case Temporary locals (age) Captured locals that outlive function (money)
Management Implicit, no GC involvement Managed by Go garbage collector
Access cost Fast, no pointer indirection Additional indirection; GC overhead
Visibility to closure Not available after return Reachable via closure function value

10. Memory visualization (CLI-style)

┌─────────────────────────────┐
│ Code segment                │
│ --------------------------- │
│ compiled code: init, main,  │
│ outer, anonymous show       │
│ (const a inlined at compile time)
└─────────────────────────────┘
           ↓
┌─────────────────────────────┐
│ Data segment                │
│ --------------------------- │
│ var p = 100                 │
└─────────────────────────────┘
           ↓
┌─────────────────────────────┐
│ Stack                       │
│ --------------------------- │
│ outer() frame               │
│   age = 30                  │
│   return address, locals    │
└─────────────────────────────┘
           ↓
┌─────────────────────────────┐
│ Heap                        │
│ --------------------------- │
│ money = 100 (for incr1)     │
│ money = 100 (for incr2)     │
└─────────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

Each returned closure function value points to the code segment (function code) and a pointer to its own heap block that holds money.


11. Garbage collector role

When closures (and references to them) go out of scope, the heap objects they reference become unreachable. Go's GC reclaims unreachable heap memory using concurrent mark-and-sweep techniques (collection timing is nondeterministic and happens automatically). From the programmer's perspective: you don't free these captured variables manually — the runtime does it when they become unreachable.


12. Minimal step-by-step trace (first outer() and two closure calls)

  1. outer() called:

    • age := 30 (stack)
    • money := 100 → marked to escape → heap allocation for money
    • fmt.Printf("Age = %d\n", age) prints Age = 30
    • show is created as a function value (code pointer + pointer to heap money)
    • outer() returns show
  2. incr1() (first call):

    • money = money + a + p100 + 10 + 100 = 210
    • prints 210
  3. incr1() (second call):

    • money = 210 + 10 + 100 = 320
    • prints 320
  4. incr2 := outer() repeats the above steps and creates a new heap cell for money for the second closure.

  5. When incr1 and incr2 become unreachable, GC reclaims their respective money blocks.


Top comments (0)