Closures in Go
Functions that capture variables from their surrounding scope, and the practical patterns they enable.
3 min read
A closure is a function that references variables from outside its own body — it "closes over" them, keeping them alive and mutable even after the outer function that declared them has returned. Go supports closures fully, and they show up constantly once you start writing idiomatic Go.
A basic closure
func makeCounter() func() int {
count := 0
return func() int {
count++
return count
}
}
counter := makeCounter()
fmt.Println(counter()) // 1
fmt.Println(counter()) // 2
fmt.Println(counter()) // 3count is declared inside makeCounter, but the anonymous function returned from it keeps a live reference to that exact variable. Each call to counter() increments the same count — it isn't reset, because the closure and count share the same underlying memory, not a copy.
Each closure gets its own captured state
counterA := makeCounter()
counterB := makeCounter()
fmt.Println(counterA()) // 1
fmt.Println(counterA()) // 2
fmt.Println(counterB()) // 1 -- independent from counterAEvery call to makeCounter() creates a fresh count variable, so counterA and counterB don't interfere with each other. This is what makes closures useful for generating independent, self-contained stateful behavior without defining a whole new type.
The classic loop variable gotcha
This used to be one of the most common Go bugs, and it's worth knowing even though modern Go (1.22+) fixed it at the language level.
funcs := make([]func(), 0)
for i := 0; i < 3; i++ {
funcs = append(funcs, func() {
fmt.Println(i)
})
}
for _, f := range funcs {
f()
}Before Go 1.22, every closure captured the same i variable (the loop reused one variable across iterations), so this would print 3, 3, 3 — surprising to anyone expecting 0, 1, 2. Go 1.22 changed loop semantics so each iteration gets its own fresh copy of i, and this now correctly prints 0, 1, 2. If you're reading or maintaining older Go code, though, you'll still see the old workaround:
for i := 0; i < 3; i++ {
i := i // shadow with a fresh copy, pre-1.22 idiom
funcs = append(funcs, func() {
fmt.Println(i)
})
}Practical uses
Closures are the mechanism behind middleware, event handlers, and the functional-options pattern from the previous lesson. A common real use is wrapping a function with extra behavior:
func withLogging(fn func(int, int) int) func(int, int) int {
return func(a, b int) int {
fmt.Printf("calling with %d, %d\n", a, b)
result := fn(a, b)
fmt.Printf("result: %d\n", result)
return result
}
}
add := func(a, b int) int { return a + b }
loggedAdd := withLogging(add)
loggedAdd(3, 4) // logs the call and result, then returns 7withLogging returns a new closure that wraps the original function, adding behavior around it without modifying add itself. This is exactly the shape HTTP middleware takes in Go web frameworks — a function that takes a handler and returns a new handler with extra behavior layered around it — which you'll recognize immediately once you get to the frameworks lesson later in this course.
Test what you just learned
4 quick questions. Get all of them right to unlock the next lesson.
You can take the quiz without an account — logging in just lets your result count toward your progress.