AI generates anonymous functions and closures constantly in Go, for HTTPWhat is http?The protocol browsers and servers use to exchange web pages, API data, and other resources, defining how requests and responses are formatted. handlers, goroutineWhat is goroutine?A lightweight concurrent function in Go launched with the go keyword - much cheaper than an OS thread (~2KB vs ~1MB of stack). launches, sort comparators, and middlewareWhat is middleware?A function that runs between receiving a request and sending a response. It can check authentication, log data, or modify the request before your main code sees it. chains. You rarely need to write them from scratch, but you absolutely need to read them. The critical skill is spotting what variables a closureWhat is closure?A function that remembers variables from the surrounding code where it was created, even after that surrounding code has finished running. captures and whether that capture creates a bug.
Anonymous functions
A function without a name. AI uses them whenever a short function is needed once:
// Assigned to a variable
double := func(x int) int {
return x * 2
}
fmt.Println(double(5)) // 10
// Passed directly as an argument
sort.Slice(users, func(i, j int) bool {
return users[i].Name < users[j].Name
})The syntax is identical to a named function minus the name. You can assign them to variables, pass them as arguments, or return them from other functions.
How closures capture variables
A closureWhat is closure?A function that remembers variables from the surrounding code where it was created, even after that surrounding code has finished running. is an anonymous functionWhat is anonymous function?A function with no name, typically assigned to a variable or passed as an argument - used frequently with arrow functions and callbacks. that references variables from its enclosing scopeWhat is scope?The area of your code where a variable is accessible; variables declared inside a function or block are invisible outside it.. The closure captures those variables by reference, not by value:
func makeCounter() func() int {
count := 0
return func() int {
count++ // captures count by reference
return count
}
}
func main() {
counter := makeCounter()
fmt.Println(counter()) // 1
fmt.Println(counter()) // 2
fmt.Println(counter()) // 3
// Independent instance, separate count
other := makeCounter()
fmt.Println(other()) // 1
}Each call to makeCounter() creates a new count variable. The returned closure holds a reference to that specific count. This is how closures maintain private state.
| Term | Definition | Key detail |
|---|---|---|
| Anonymous function | Function with no name | Can be assigned, passed, returned |
| Closure | Anonymous function + captured variables | Variables are captured by reference |
| Captured variable | Variable from outer scope used inside closure | Shared between closure and outer scope |
| IIFE | Immediately invoked function expression | func() { ... }(), runs right away |
Where AI uses closures in Go
These are the four patterns you'll see most in AI-generated Go:
HTTPWhat is http?The protocol browsers and servers use to exchange web pages, API data, and other resources, defining how requests and responses are formatted. handlers with injected dependencies
func makeUserHandler(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// closure captures db
rows, err := db.Query("SELECT * FROM users")
if err != nil {
http.Error(w, err.Error(), 500)
return
}
defer rows.Close()
// ...
}
}The closureWhat is closure?A function that remembers variables from the surrounding code where it was created, even after that surrounding code has finished running. captures db so the handler has access to the database without global variables.
GoroutineWhat is goroutine?A lightweight concurrent function in Go launched with the go keyword - much cheaper than an OS thread (~2KB vs ~1MB of stack). launches
for _, url := range urls {
go func(u string) {
resp, err := http.Get(u)
// ... process resp
}(url) // pass url as argument
}Sort comparators
sort.Slice(products, func(i, j int) bool {
return products[i].Price < products[j].Price
})MiddlewareWhat is middleware?A function that runs between receiving a request and sending a response. It can check authentication, log data, or modify the request before your main code sees it. chains
func withAuth(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("Authorization")
if token == "" {
http.Error(w, "unauthorized", 401)
return
}
next.ServeHTTP(w, r)
})
}The loop variable capture bug
This is the most famous Go gotcha, and AI still generates it. In Go versions before 1.22, loop variables were reused across iterations:
// BUGGY in Go < 1.22
for i := 0; i < 3; i++ {
go func() {
fmt.Println(i) // captures i by reference
}()
}
// Likely prints: 3, 3, 3 (not 0, 1, 2)All three goroutines capture the same i. By the time they run, the loop has finished and i is 3.
The fix (works in all Go versions):
for i := 0; i < 3; i++ {
go func(n int) {
fmt.Println(n) // n is a copy, not a reference
}(i) // pass i as argument - creates a copy
}Go 1.22 changed the loop variable semantics so each iteration gets its own variable. But AI trained on older code still generates the buggy pattern, and plenty of codebases still target Go < 1.22.
1. Goroutines inside loops without passing the variable as an argument
2. Deferred closures inside loops that reference the loop variable
Even if you're targeting Go 1.22+, the parameter-passing pattern is still clearer and worth requesting.
| Go version | Loop variable behavior | Safe pattern |
|---|---|---|
| < 1.22 | Single variable reused across iterations | Pass as function argument: go func(n int) { ... }(i) |
| >= 1.22 | New variable per iteration | Either pattern works, but argument passing is clearer |
IIFEs, immediately invoked functions
Sometimes you need to run a function immediately after defining it. This creates an isolated scopeWhat is scope?The area of your code where a variable is accessible; variables declared inside a function or block are invisible outside it.:
config := func() map[string]string {
defaults := map[string]string{
"host": "localhost",
"port": "8080",
}
if envHost := os.Getenv("HOST"); envHost != "" {
defaults["host"] = envHost
}
return defaults
}() // the () invokes it immediatelyIIFEs keep temporary variables (defaults) out of the surrounding scope. AI uses this pattern for complex initialization where a simple assignment isn't enough.
When closures vs named functions
Closures add cognitive load because you have to trace captured variables. Use this as a guide when evaluating AI's choice:
| Situation | Better choice | Why |
|---|---|---|
| Short, used once | Closure | No need to name a throwaway function |
| Needs captured state | Closure | That's what closures are for |
| Complex logic (10+ lines) | Named function | Easier to test, read, debug |
| Used in multiple places | Named function | DRY, don't duplicate closures |
| Inside a performance-critical loop | Named function | Avoid repeated closure allocation |