
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
- Starting with a code Example
- Program output
- What is a closure
- Why closures matter
- Compiler time: code/data segments & compilation flow
- Key segments (table)
- Escape analysis — what it is and how it decides
- Runtime behavior — stepwise
- Program startup
- Call to
outer() - Invoking the closure
- Multiple closures
- End of life
- Stack vs Heap — compact comparison table
- Memory visualization (CLI)
- 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()
}
2. Program output
Bangla Bank
Age = 30
210
320
Age = 30
210
320
Note: two separate closures created by two calls to
outer()(incr1andincr2), each with its ownmoneyvalue 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 whyincr1()andincr2()maintain separatemoneyvalues. - 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:
-
ageis used only insideouter()and is never referenced byshow→ stays on the stack. -
moneyis referenced byshowandshowis returned →moneyescapes and is allocated on the heap.
8. Runtime behavior- function values, stack frames, heap objects
Program startup
- Package
init()runs (printsBangla Bank). - Program loads code/data segments and prepares to execute
main().
Call to outer()
- A stack frame for
outer()is created. -
age := 30is allocated as a stack-local candidate. -
money := 100is 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
showhas 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
moneycell. - 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 formoney. -
incr1andincr2therefore refer to different heap objects; theirmoneystates 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) │
└─────────────────────────────┘
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)
-
outer()called:-
age := 30(stack) -
money := 100→ marked to escape → heap allocation formoney -
fmt.Printf("Age = %d\n", age)printsAge = 30 -
showis created as a function value (code pointer + pointer to heapmoney) -
outer()returnsshow
-
-
incr1()(first call):-
money = money + a + p→100 + 10 + 100 = 210 - prints
210
-
-
incr1()(second call):money = 210 + 10 + 100 = 320- prints
320
incr2 := outer()repeats the above steps and creates a new heap cell formoneyfor the second closure.When
incr1andincr2become unreachable, GC reclaims their respectivemoneyblocks.
Top comments (0)