Closures in Go: Callbacks & Higher-order functions
Practical Go examples: callbacks, higher-order functions (HOFs), and how they relate to closures and private state.
In Go, functions are first-class: you can pass them as arguments, return them, and store them in variables. That makes callbacks and higher-order functions natural and useful patterns — and they pair perfectly with closures for encapsulating state.
1. Callbacks in Go
A callback is a function you pass to another function so it can be invoked later. This is common for async code using goroutines or for event handlers.
// callback.go
package main
import (
"fmt"
"time"
)
// fetchData performs work asynchronously and invokes cb when done.
func fetchData(id int, cb func(result string)) {
go func() {
// simulate work
time.Sleep(100 * time.Millisecond)
cb(fmt.Sprintf("data-%d", id))
}()
}
func main() {
done := make(chan struct{})
fetchData(7, func(r string) {
fmt.Println("Got:", r)
close(done)
})
// wait for callback
<-done
}What acts: fetchData starts a goroutine to do the work. That goroutine is the actor that, after finishing, calls the provided callback cb with the result.
How it coordinates: The caller provides a callback that prints the result and closes the done channel. The main goroutine waits on <-done so the program doesn't exit before the callback runs. This pattern keeps the async code simple, but you must avoid goroutine leaks and ensure the callback doesn't block forever.
Pitfalls to watch: calling callbacks from background goroutines means the callback runs concurrently — avoid accessing non-thread-safe shared state inside the callback without synchronization.
2) Higher-order functions (HOFs)
HOFs accept functions and/or return functions. They’re great for adding cross-cutting behavior (logging, retries, metrics) or creating tailored function factories. HOFs are often implemented by returning a closure that captures some state.
// hof.go
package main
import "fmt"
// withLogging is a higher-order function: it accepts a function and returns a wrapped function.
func withLogging(fn func(int) int) func(int) int {
return func(arg int) int {
fmt.Println("calling with", arg)
res := fn(arg)
fmt.Println("returned", res)
return res
}
}
func square(n int) int { return n * n }
func main() {
loggedSquare := withLogging(square)
fmt.Println(loggedSquare(4)) // prints logs then 16
}What acts: withLogging returns a wrapper function. The returned wrapper (the actor) calls the original fn, logs before and after, and returns the result.
How it captures state: In this example the wrapper closes over fn — the original function — but there is no mutable shared state. You can easily extend the pattern to capture configuration (like a log prefix) or counters.
Why use it: HOFs let you add behavior without changing the original function implementation. They compose nicely and keep concerns separated.
Memoization — a HOF that keeps state
HOFs can hold private state via closures. The memoize example below returns a function that caches results in a map that only the returned function can access.
// memoize.go
package main
import "fmt"
// memoize returns a function that caches results of a pure int->int function.
func memoize(fn func(int) int) func(int) int {
cache := map[int]int{}
return func(x int) int {
if v, ok := cache[x]; ok {
return v
}
v := fn(x)
cache[x] = v
return v
}
}
func slowDouble(n int) int {
// pretend this is expensive
return n * 2
}
func main() {
fast := memoize(slowDouble)
fmt.Println(fast(5)) // computes
fmt.Println(fast(5)) // cached
}What acts: the returned function both computes and serves cached results. It is the single actor responsible for reading and writing the private cache map.
How the closure protects state: the cache variable is declared inside the factory and is inaccessible from outside — only the returned function can touch it. That provides safe encapsulation at the language level.
Concurrency note: the simple memoize above is not safe for concurrent use. If multiple goroutines call the returned function, protect access to cache with a sync.Mutex or use an alternative concurrency-safe strategy.