Closures in Go: A Beginner-Friendly Introduction
Simple, practical examples that show how to keep small state local and safe.
A closure is just a function that keeps access to variables from where it was created. This lets you build small helpers with private state without exposing internal variables.
1) Minimal example — a counter
The returned function remembers the variable i. Each call increments it.
// counter.go
package main
import "fmt"
func counter(start int) func() int {
i := start
return func() int {
i++
return i
}
}
func main() {
c := counter(0)
fmt.Println(c()) // 1
fmt.Println(c()) // 2
}Here, counter returns a function that still has access to i. That’s the entire idea of closures.
2) A practical API — returning Inc + Peek
Instead of exposing the value, return a clean API. Here we expose Inc and Peek while keeping the counter private.
// encapsulate.go
package main
import "fmt"
type counterAPI struct {
Inc func() int
Peek func() int
}
func newCounter(start int) counterAPI {
i := start
inc := func() int {
i++
return i
}
peek := func() int {
return i
}
return counterAPI{Inc: inc, Peek: peek}
}
func main() {
c := newCounter(5)
fmt.Println(c.Peek()) // 5
fmt.Println(c.Inc()) // 6
fmt.Println(c.Peek()) // 6
}This keeps internal state hidden and forces callers to use a consistent API.
Concurrency note
These examples are not concurrency-safe. Multiple goroutines updating i will race. To fix that, wrap a sync.Mutex inside the factory.
When closures make sense
Use closures for small helpers and factories. For bigger APIs or anything requiring interfaces, prefer a named struct with methods.