AlgoMaster Logo

Closures

Last Updated: May 22, 2026

High Priority
6 min read

A closure is a function value that remembers variables from the scope where it was created, even after that scope has returned. Closures let you build small, stateful helpers without declaring a struct or wiring up a global. They're the foundation for factory functions, middleware, and the on-the-fly callbacks you'll see all over real Go codebases.

What a Closure Actually Is

A function in Go is a first-class value. When you write a function literal (also called an anonymous function) inside another function, the inner function can use variables declared in the outer one. The inner function plus those captured variables is the closure.

The greet function never received customerName as a parameter, but it still uses it. The variable was captured from the surrounding scope. And notice the second call: it printed Riley, not Alex. Closures capture variables, not snapshots of their values. Whatever the variable holds at the moment the closure runs is what the closure sees.

Capturing happens by reference. The closure and the surrounding function are looking at the same variable, not at two copies of it.

Captured Variables Outlive the Enclosing Function

The interesting case is when the outer function returns. In most languages with stack-allocated locals, the captured variables would be invalid the moment the outer function exits. Go handles this differently. If a local variable is captured by a closure that escapes the function, the compiler moves that variable to the heap so it stays alive as long as the closure does. This is called escape analysis, and you don't have to do anything to enable it.

makeCounter declares count as a local variable and returns a function that increments and returns it. After makeCounter returns, the local count would normally be gone. But because the returned closure still references it, the compiler keeps it alive. Every call to next() reads and updates the same count.

Factory Functions: Configured Closures

Once a closure can capture configuration, factory functions follow naturally. You write a function that takes some settings, builds a closure around them, and returns the closure. Each call to the factory produces a fresh, independently configured function.

makeDiscount(0.10) returns a closure that has its own rate set to 0.10. makeDiscount(0.25) returns a different closure with rate set to 0.25. The two closures don't share anything. Each one carries its own captured rate around.

This is a common use of closures in Go: configure a function once, then pass it around as a reusable callable. Compare it to passing rate as a second argument to every call site. The factory pattern hides the configuration inside the function value, so call sites only deal with the price.

Order ID Generators

A closure that counts is a common pattern that shows up in real code. An order ID generator hands out monotonically increasing IDs without exposing the counter or needing a global.

Both prefix and id are captured. prefix never changes, so each generated ID starts with the same string. id is mutated on every call, and the mutation persists because the closure keeps the variable alive.

If two parts of the program need IDs from independent sequences, you call the factory twice and get two closures, each with its own id.

The two generators don't interfere. Each one captured its own id. This is what people mean when they say closures encapsulate state.

Sharing State Between Closures

Sometimes you want the opposite: two closures that share the same captured variable. That happens automatically when both closures are created in the same scope and reference the same name.

add and current both close over the same stock variable. Mutations through add are visible to current. This is the closure equivalent of giving two methods access to the same private field, without writing a struct.

How Capture Works (Visualized)

Picture what's happening when a closure escapes its enclosing function. makeCounter declares count on the stack, but because the returned closure still references it, the compiler moves count to the heap. The closure value carries a reference to that heap-allocated variable.

There's one count, and the closure holds a reference to it. That's why repeated calls to next() see the updated value, and why two closures created in the same makeCounter call would share state, while closures from two separate makeCounter calls would not.

The Loop Variable Capture Gotcha

Closures inside a loop have one well-known trap, and the rules around it changed in Go 1.22. Understanding both the old behavior and the new one matters, because a large amount of code still runs on older Go versions.

The pre-Go 1.22 behavior. Before Go 1.22, the loop variable in a for statement was declared once per loop, and every iteration reused it. A closure created inside the loop captured that one shared variable. By the time you ran the closures, the loop was over and the variable held its last value.

On Go 1.21 and earlier, this prints:

All three closures captured the same i and the same p. By the time they ran, the loop had finished, leaving i = 2 and p = "Monitor". The fix on older Go was to rebind the variable inside the loop body:

The line i, p := i, p creates new variables with the same names, scoped to the current iteration. Each closure now captures its own pair.

The Go 1.22+ fix. Go 1.22 changed the language. Loop variables in for statements are now scoped per iteration. Each iteration gets fresh variables, and closures capture the iteration's own copies.

On Go 1.22 or later, this prints the expected output:

Same code, different output, depending on the Go version. The change is controlled by the go directive in go.mod: a module with go 1.22 or higher gets the new behavior, and a module on go 1.21 or earlier keeps the old behavior. This was done so existing code wouldn't silently change meaning when upgraded.

If you're writing new code today against Go 1.22 or later, you can stop worrying about the trap. If you're reading or maintaining older code, or your go.mod still says go 1.21, you need the rebinding trick.

Practical Uses

Closures show up in three patterns more than anywhere else.

Configuration baked into a callable. The makeDiscount example above shows this. Anywhere a function needs to behave differently based on settings (a tax rate, a feature flag, a logger), wrapping the settings in a closure beats threading them through every call.

Per-customer or per-resource lookups. A function that looks up the discount rate for one customer can return a closure tailored to that customer.

Middleware and wrappers. Web frameworks and pipelines often chain functions that wrap other functions. A logging middleware that takes a handler and returns a handler is a closure: the returned function captures the original handler and adds behavior around it.