13. Closures & Iterators
📋 Jump to Takeaways🎁 Functions that remember variables from their enclosing scope — it sounds like magic, but it's exactly how every Go middleware, callback, and iterator works under the hood. Master closures and you unlock some of Go's most powerful patterns.
Functions as Values
In Go, functions are first-class citizens. You can assign them to variables, store them in slices, and pass them as arguments to other functions — just like any other value.
func double(n int) int {
return n * 2
}
func main() {
// Assign a function to a variable
op := double
fmt.Println(op(5)) // 10
// Pass a function as an argument
result := apply(3, double)
fmt.Println(result) // 6
}
func apply(n int, fn func(int) int) int {
return fn(n)
}The type of a function variable is its signature. op has type func(int) int. You can call it exactly like the original function.
Anonymous Functions
You don't always need to name a function. Anonymous functions let you define logic inline, right where you need it.
func main() {
greet := func(name string) string {
return "Hello, " + name
}
fmt.Println(greet("Gopher")) // Hello, Gopher
// Immediately invoked
result := func(a, b int) int {
return a + b
}(3, 4)
fmt.Println(result) // 7
}Anonymous functions are perfect for short-lived logic that doesn't deserve a top-level name — think callbacks, goroutines, and deferred cleanup.
Closures: Capturing Outer Variables
A closure is a function that references variables from outside its own body. The function "closes over" those variables, keeping them alive even after the surrounding function returns.
func counter() func() int {
count := 0
return func() int {
count++ // captures `count` from the enclosing scope
return count
}
}
func main() {
next := counter()
fmt.Println(next()) // 1
fmt.Println(next()) // 2
fmt.Println(next()) // 3
}Each call to counter() creates a fresh count variable. The returned function holds a reference to that specific count, so the state persists between calls. This is how closures provide encapsulation without structs.
The Closure Gotcha in Loops
One of Go's most common bugs involves closures inside loops. When you launch goroutines in a loop, the closure captures the variable — not the value at that moment.
func main() {
names := []string{"Alice", "Bob", "Carol"}
for _, name := range names {
go func() {
fmt.Println(name) // BUG: might print "Carol" three times
}()
}
time.Sleep(time.Second)
}By the time the goroutines execute, the loop has finished and name holds its final value. Fix this by passing the variable as a parameter:
for _, name := range names {
go func(n string) {
fmt.Println(n) // Correct: each goroutine gets its own copy
}(name)
}Note: Starting in Go 1.22, loop variables are scoped per-iteration, eliminating this gotcha for new code. But you'll still encounter the pattern in older codebases.
Practical Closures
Closures power some of Go's most idiomatic patterns. Here are three you'll use constantly.
Middleware pattern:
func logging(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
log.Printf("%s %s", r.Method, r.URL.Path)
next(w, r) // closes over `next`
}
}sort.Slice comparator:
people := []struct{ Name string; Age int }{
{"Alice", 30}, {"Bob", 25}, {"Carol", 35},
}
sort.Slice(people, func(i, j int) bool {
return people[i].Age < people[j].Age // closes over `people`
})
// [{Bob 25} {Alice 30} {Carol 35}]http.HandleFunc:
prefix := "/api"
http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "serving under %s", prefix) // closes over `prefix`
})In each case, the closure captures external state and carries it into a callback context. No globals, no extra structs — just a function with memory.
Iterating with Closures: The Iterator Pattern
You can build custom iterators using closures. The pattern returns a function that yields the next value and a boolean indicating whether iteration should continue.
func fibIterator() func() (int, bool) {
a, b := 0, 1
return func() (int, bool) {
a, b = b, a+b
return a, true // infinite iterator
}
}
func main() {
next := fibIterator()
for i := 0; i < 7; i++ {
val, _ := next()
fmt.Print(val, " ") // 1 1 2 3 5 8 13
}
}This func() (T, bool) pattern gives you lazy evaluation — values are computed on demand instead of being generated all at once. It's perfect for large datasets, infinite sequences, or expensive computations.
Range Over Built-in Types
Go's range keyword works with slices, maps, channels, and strings out of the box.
// Slice — index and value
nums := []int{10, 20, 30}
for i, v := range nums {
fmt.Printf("[%d]=%d ", i, v) // [0]=10 [1]=20 [2]=30
}
// Map — key and value (random order)
m := map[string]int{"a": 1, "b": 2}
for k, v := range m {
fmt.Printf("%s:%d ", k, v) // a:1 b:2 (order varies)
}
// Channel — values until closed
ch := make(chan int, 3)
ch <- 1; ch <- 2; ch <- 3; close(ch)
for v := range ch {
fmt.Print(v, " ") // 1 2 3
}
// String — rune index and rune
for i, r := range "Go🚀" {
fmt.Printf("%d:%c ", i, r) // 0:G 1:o 2:🚀
}Use _ to discard the index or value when you don't need it.
The Iterator Pattern in Go: Past and Future
Before Go 1.23, custom iteration required the manual func() (T, bool) closure pattern shown above, or you returned a channel (which adds goroutine overhead). Libraries like bufio.Scanner used a method-based approach: Scan() bool + Text() string.
Go 1.23 introduced range-over-func — you can now use range directly on iterator functions:
// Go 1.23+ iterator signature
func Backward(s []string) iter.Seq2[int, string] {
return func(yield func(int, string) bool) {
for i := len(s) - 1; i >= 0; i-- {
if !yield(i, s[i]) {
return
}
}
}
}
for i, v := range Backward([]string{"a", "b", "c"}) {
fmt.Println(i, v) // 2 c, 1 b, 0 a
}The iter.Seq[V] and iter.Seq2[K, V] types in the standard library formalize the pattern. Under the hood, it's still closures all the way down — the language just gives you cleaner syntax now.
Key Takeaways
- Functions are values in Go — assign them to variables, pass them as arguments, return them from other functions.
- Anonymous functions define logic inline without a name.
- A closure captures variables from its enclosing scope, keeping them alive across calls.
- Watch out for the loop-variable gotcha: pass the variable as a parameter or use Go 1.22+ per-iteration scoping.
- Middleware, comparators, and HTTP handlers are the most common real-world closure patterns.
- The
func() (T, bool)closure pattern gives you lazy, pull-based iteration. rangeworks natively on slices, maps, channels, and strings.- Go 1.23's range-over-func lets you use
rangeon custom iterator functions viaiter.Seq.
🎁 You now have closures and iterators in your toolkit — two patterns that show up everywhere in production Go. Next up: you'll put everything together in a capstone project where you build a concurrent file scanner that walks directories, matches patterns, and reports results — closures, goroutines, channels, and all.