Closures in Go
Closures are functions that capture variables from their surrounding scope. They enable stateful functions, factories, and composition.
Core ideas
- Capture: inner functions can access outer variables
- Lifetime: captured variables live with the closure
- State: closures provide private, persistent state without globals
- By reference: captured variables are shared between closures and the outer scope
Basic capture
package main
import "fmt"
func Greeter(name string) func() {
// name is captured
return func() { fmt.Println("Hello,", name) }
}
func main() {
g := Greeter("Alice")
g() // Hello, Alice
}
Stateful closures
package main
import "fmt"
func Counter(start int) func() int {
n := start
return func() int {
n++
return n
}
}
func main() {
c := Counter(0)
fmt.Println(c(), c(), c()) // 1 2 3
}
Loop variable capture (pitfall and fix)
Each iteration of for range reuses the same loop variable. Copy it (shadow) or pass as a parameter when capturing.
package main
import "fmt"
func main() {
// BAD: all closures print the same final i
var bad []func()
for i := 0; i < 3; i++ {
bad = append(bad, func() { fmt.Println("bad:", i) })
}
for _, f := range bad { f() }
// GOOD: shadow i or pass as parameter
var good []func()
for i := 0; i < 3; i++ {
i := i
good = append(good, func() { fmt.Println("good:", i) })
}
for _, f := range good { f() }
}
Function factories
package main
import (
"fmt"
"strings"
)
func MakeValidator(min int, required bool) func(string) error {
return func(s string) error {
if required && strings.TrimSpace(s) == "" {
return fmt.Errorf("required")
}
if len(s) < min { return fmt.Errorf("min %d", min) }
return nil
}
}
func main() {
validate := MakeValidator(3, true)
fmt.Println(validate("ok")) // <nil>
fmt.Println(validate(" ")) // required
}
Memoization (simple cache)
Closures can maintain caches across invocations for expensive computations.
package main
import "fmt"
func Memoize(f func(int) int) func(int) int {
cache := map[int]int{}
return func(n int) int {
if v, ok := cache[n]; ok { return v }
v := f(n)
cache[n] = v
return v
}
}
func Fib(n int) int {
if n < 2 { return n }
return Fib(n-1) + Fib(n-2)
}
func main() {
mfib := Memoize(Fib)
fmt.Println(mfib(10))
}
Middleware-like composition
Wrap a handler with cross-cutting behavior (logging, timing, metrics) without changing core logic.
package main
import (
"fmt"
"time"
)
type Handler func(string) string
type Middleware func(Handler) Handler
func Log(next Handler) Handler {
return func(s string) string {
t := time.Now()
out := next(s)
fmt.Printf("took %v\n", time.Since(t))
return out
}
}
func Compose(ms ...Middleware) Middleware {
return func(next Handler) Handler {
for i := len(ms)-1; i >= 0; i-- { next = ms[i](next) }
return next
}
}
func Echo(s string) string { return "echo: " + s }
func main() {
h := Compose(Log)(Echo)
fmt.Println(h("hello"))
}
Best practices
- Capture only what you need (avoid holding large structs)
- Watch loop variable capture in for-loops and goroutines
- Prefer small closures; use named functions for complex logic
- Document stateful behavior and concurrency expectations
- Avoid capturing pointers to short-lived data that may be mutated elsewhere
Summary
- Closures enable local state, factories, and composition
- Handle capture semantics carefully to avoid bugs and leaks
- Use when per-function configuration/state is needed