Closures, Lexical Scope & Defer in Go 🎒
In Go, functions are first-class citizens: you can assign them to variables, pass them as arguments, return them from other functions, and capture variables from their outer scope (Closures).
Paired with Go’s built-in defer statement, these features provide clean, guaranteed resource cleanup and modular architecture.
1. What is a Closure?
A Closure is an anonymous function that references variables from outside its body. The function can access and modify the referenced variables; the variables are bound to the function.
package main
import "fmt"
// intSeq returns a closure that increments its own private counter
func intSeq() func() int {
i := 0 // Captured in the closure's scope
return func() int {
i++
return i
}
}
func main() {
// nextInt captures its own independent 'i'
nextInt := intSeq()
fmt.Println(nextInt()) // 1
fmt.Println(nextInt()) // 2
fmt.Println(nextInt()) // 3
// A new call to intSeq creates a completely independent closure!
newSeq := intSeq()
fmt.Println(newSeq()) // 1
}2. Real-World Use Case: Configurable HTTP Middleware
In Go web services, closures are the standard way to build HTTP middleware with configuration:
package main
import (
"fmt"
"net/http"
"time"
)
// WithRateLimit returns an HTTP middleware closure with configured limit
func WithRateLimit(maxPerSecond int) func(http.Handler) http.Handler {
// Shared state across requests
tokens := maxPerSecond
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if tokens <= 0 {
http.Error(w, "Rate limit exceeded", http.StatusTooManyRequests)
return
}
tokens--
next.ServeHTTP(w, r)
})
}
}3. The defer Statement: Guaranteed Cleanup
defer schedules a function call to run immediately before the surrounding function returns, regardless of whether the function returns normally or panics.
1. Basic LIFO Execution Order
Deferred calls are executed in Last-In, First-Out (LIFO) order (like a stack):
package main
import "fmt"
func main() {
defer fmt.Println("1st defer (runs last)")
defer fmt.Println("2nd defer")
defer fmt.Println("3rd defer (runs first)")
fmt.Println("Main execution finished")
}
// Output:
// Main execution finished
// 3rd defer (runs first)
// 2nd defer
// 1st defer (runs last)2. The #1 Use Case: Releasing Resources
Always call defer immediately after successfully opening a resource (file, mutex lock, database row cursor, HTTP response body):
func processFile(filename string) error {
f, err := os.Open(filename)
if err != nil {
return err
}
defer f.Close() // Guaranteed to close, even if code below returns early or errors!
data, err := io.ReadAll(f)
if err != nil {
return err
}
return handle(data)
}3. Timing Function Execution with Defer
Because arguments to a deferred function are evaluated immediately when the defer line is encountered, closures allow elegant execution timing:
func measureLatency() {
start := time.Now()
defer func() {
fmt.Printf("Operation took: %v\n", time.Since(start))
}()
time.Sleep(50 * time.Millisecond) // Simulated work
}4. Loop Variable Capture in Goroutines (The Go 1.22 Fix)
In Go versions prior to 1.22, loop variables were reused across iterations, leading to a notorious bug when spawning goroutines or closures inside loops:
package main
import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
items := []string{"A", "B", "C"}
for _, item := range items {
wg.Add(1)
go func() {
defer wg.Done()
fmt.Println(item)
}()
}
wg.Wait()
}- Go 1.21 and older: Frequently printed
C, C, Cbecause all goroutines captured the single shareditempointer. - Go 1.22+: The Go compiler creates a new instance of the variable on every loop iteration, so it safely prints
A,B, andC!
Across the Wiki
- Scope, Closures & Callbacks — Language-agnostic closure concepts.
- Go Channels — Coordinating goroutines spawned with closures.
- Go Sync Package — Using
defer mu.Unlock()with Mutexes.