Go 闭包

Go 中闭包是捕获外部变量引用的函数值,常用于中间件、回调和并发控制,但需注意变量捕获的时机陷阱。

#type / concept #status / growing #tech / dev #resource / go

[!info] related notes

Go 闭包

一句话定义

闭包是一个函数值,它捕获并持有其定义作用域中外部变量的引用——即使外部函数已返回,闭包仍可通过该引用读写这些变量。

核心机制 / 工作原理

  1. 函数是第一类值:Go 中函数可以赋值给变量、作为参数传递、作为返回值返回。函数类型写作 func(参数) 返回值,如 func(int) string

  2. 闭包捕获变量引用,而非值:当一个内部函数引用了外部函数的变量,Go 编译器会将该变量”逃逸”到堆上,闭包持有对它的引用。多次调用外部函数产生的多个闭包共享(或各自持有)对应的变量实例。

  3. goroutine 循环变量的经典陷阱

    for i := 0; i < 5; i++ {
        go func() {
            fmt.Println(i) // 所有 goroutine 可能都打印 5
        }()
    }

    所有闭包捕获的是同一个 i 的引用,循环结束后 i 的值是 5。Go 1.22 起修复了此问题——循环变量每次迭代创建新实例。但在 1.22 之前的版本或等价场景中,修复方式是:

    for i := 0; i < 5; i++ {
        i := i // 重新声明,创建新变量
        go func() {
            fmt.Println(i) // 各自打印 0,1,2,3,4
        }()
    }
  4. defer + 闭包的求值时机defer 语句中的闭包在 defer 注册时捕获变量的引用(不是值),但闭包体在 defer 执行时(函数返回前)才运行。

    func f() int {
        x := 0
        defer func() { x++ }() // 捕获 x 的引用
        return x                // 返回 0(返回值先被赋值),但 x 被 defer 改为 1(不影响返回值)
    }
  5. 函数类型func(T) R 是一个类型。可以声明变量、用作结构体字段、定义类型别名。零值是 nil

最小例子 / 最小场景

package main

import "fmt"

// 计数器工厂 — 经典闭包用法
func counter(start int) func() int {
    n := start
    return func() int {
        n++        // 捕获外部的 n
        return n
    }
}

// 中间件模式
func withLogging(fn func(string)) func(string) {
    return func(msg string) {
        fmt.Println("[LOG] calling with:", msg)
        fn(msg)
        fmt.Println("[LOG] done")
    }
}

func main() {
    // 闭包保持状态
    c1 := counter(0)
    c2 := counter(100)
    fmt.Println(c1(), c1(), c1()) // 1 2 3
    fmt.Println(c2(), c2())       // 101 102

    // 作为中间件
    greet := func(name string) {
        fmt.Println("Hello,", name)
    }
    loggedGreet := withLogging(greet)
    loggedGreet("World")
    // [LOG] calling with: World
    // Hello, World
    // [LOG] done
}

为什么重要

  • HTTP 中间件的标准模式:Go web 框架(标准库 http.Handler、chi、echo 等)的中间件本质就是接收 Handler 返回 Handler 的闭包链。
  • 并发控制:闭包常与 goroutine 配合,封装工作单元。sync.WaitGrouperrgroupcontext 传参都依赖闭包捕获。
  • 函数式编程模式map/filter/reduce、装饰器、策略模式——闭包使得这些模式在 Go 中自然实现。
  • 资源封装defer 中使用闭包可以精确控制清理逻辑中需要访问的变量。

边界与易混淆点

  • 捕获的是引用,不是快照:闭包看到的始终是变量的最新值。如果需要快照,必须在创建闭包时将值拷贝到新变量(v := v 惯用法)。
  • Go 1.22 循环变量语义变更:1.22 起 for 循环变量每次迭代是新实例,go func(){ use(i) }() 不再有竞态问题。但 for rangevfor i := ... 的行为差异仍需理解。
  • 闭包逃逸到堆上:闭包引用的变量如果原本在栈上,会被编译器提升到堆上(逃逸分析)。小函数中的简单变量被闭包捕获会增加堆分配。
  • nil 函数值调用会 panicvar fn func(); fn() 会 panic,必须确保函数值已初始化。
  • 闭包不利于序列化:函数值不能 JSON 序列化、不能跨进程传递。如果需要可传输的回调,用函数注册表(map[string]func)替代。
创建于 2026/6/25 更新于 2026/6/25