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 internally. 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
An anonymous function is a function literal with no name. It's truly anonymous only when you use it without assigning it to a variable, like calling it immediately or passing it directly as an argument.
func main() {
// Immediately invoked — never stored, never named
func() {
fmt.Println("runs once and disappears")
}()
// Passed directly as an argument — no variable needed
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("hello"))
})
// Launched as a goroutine
go func() {
fmt.Println("running in background")
}()
}If you assign a function literal to a variable (greet := func()...), it's practically the same as naming it. The real power of anonymous functions is using them inline: callbacks, goroutines, and deferred cleanup where a top-level name would just add noise.
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)
}The parameter approach also has a performance benefit: passing by value avoids a heap allocation per iteration, since the variable doesn't need to escape. Even in Go 1.22+ where loop variables are scoped per-iteration, the parameter pattern is still the better choice.
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
The func() (T, bool) closure pattern works, but it has a limitation: you can't use Go's range keyword with it. You're stuck writing a manual for loop and calling next() yourself.
Before Go 1.23, the alternatives weren't great either. You could push values through a channel, but that requires a goroutine and carries overhead. Standard library types like bufio.Scanner used a method pair: Scan() bool to advance, Text() string to read — which works but isn't composable.
Go 1.23 fixed this with range-over-func. You can now write a function that range knows how to iterate.
The trick is a parameter called yield by convention — it's just a function your iterator calls for each value. If yield returns false, the caller broke out of the loop early and your iterator should stop:
import "iter"
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]) { // yield returns false on break
return
}
}
}
}
for i, v := range Backward([]string{"a", "b", "c"}) {
fmt.Println(i, v)
}
// 2 c
// 1 b
// 0 aThe single return in Backward returns the closure itself — it just hands the function to range. The iteration values don't come from a return at all. They're passed as arguments to yield:
yield(i, s[i]) // pushes i and s[i] into the loop variablesWhen range calls your iterator, it wires yield to the loop body. Each yield(i, s[i]) call delivers those values to i and v in for i, v := range Backward(...). When the caller hits break, yield returns false and your iterator returns early. Values travel through yield's arguments, not through return.
yield is not a keyword — it's just the name the community settled on for the callback parameter. The compiler recognizes functions with the signature func(yield func(V) bool) as rangeable.
iter.Seq[V] is a single-value iterator (like ranging over a slice with _ for the index). iter.Seq2[K, V] yields two values per step, like index + value. Internally it's still closures — the language just gives you range syntax on top.
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.
- Pass loop variables as parameters to closures: it fixes correctness and avoids a heap allocation per iteration.
- 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 using closures, goroutines, and channels.