首页 / Go 语言入门教程 / 匿名函数与闭包

Go 语言入门教程

匿名函数与闭包

本教程共 80 篇 · 第 24 篇 · 更新于 2026-07-27 · 约 8 分钟阅读

GoGo 入门教程匿名函数闭包closure函数字面量Go 函数

24. 匿名函数与闭包

本节目标:掌握匿名函数的写法,理解闭包的捕获机制,学会避免闭包的常见陷阱。

匿名函数

匿名函数就是没有名字的函数。Go 里叫函数字面量(Function literal)。

基本写法

func(x, y int) int {
    return x + y
}

这就是一个匿名函数。它没有名字,但定义了参数和返回值。

立即执行

result := func(x, y int) int {
    return x + y
}(3, 4)

fmt.Println(result)  // 7

在函数后面直接加 (3, 4) 就立即调用。这种模式叫 IIFE(Immediately Invoked Function Expression)。

赋值给变量

add := func(x, y int) int {
    return x + y
}

fmt.Println(add(3, 4))  // 7

赋值给变量后,就可以用变量名来调用了。这和用 func 声明函数的效果类似,但写法更灵活。

作为参数

func apply(f func(int, int) int, a, b int) int {
    return f(a, b)
}

func main() {
    result := apply(func(x, y int) int {
        return x * y
    }, 3, 4)
    fmt.Println(result)  // 12
}

直接在调用时定义一个匿名函数传进去。

作为返回值

func multiplier(factor int) func(int) int {
    return func(x int) int {
        return x * factor
    }
}

func main() {
    double := multiplier(2)
    triple := multiplier(3)

    fmt.Println(double(5))  // 10
    fmt.Println(triple(5))  // 15
}

multiplier 返回一个函数,这个函数「记住」了 factor 的值。这就是闭包。

闭包

闭包(Closure)是匿名函数加上它引用的外部变量。

闭包捕获外部变量

func main() {
    count := 0

    increment := func() {
        count++  // 捕获了外部的 count
    }

    increment()
    increment()
    increment()

    fmt.Println(count)  // 3
}

increment 函数引用了外部的 count 变量。每次调用 incrementcount 都会加 1。这就是闭包:函数「关闭」了它引用的外部变量,形成了封闭的作用域。

闭包捕获的是引用

闭包捕获的是变量的引用,不是值的副本:

func main() {
    x := 10

    f := func() {
        fmt.Println(x)
    }

    x = 20
    f()  // 20,不是 10
}

f 捕获的是 x 的引用。当 x 变成 20 后,f() 输出的也是 20。

闭包实现计数器

func makeCounter() func() int {
    count := 0
    return func() int {
        count++
        return count
    }
}

func main() {
    counter := makeCounter()

    fmt.Println(counter())  // 1
    fmt.Println(counter())  // 2
    fmt.Println(counter())  // 3
}

count 变量在 makeCounter 返回后,外部无法直接访问,但闭包 counter 仍然能操作它。这是一种封装数据的方式。

多个闭包共享变量

func makePair() (func() int, func(int)) {
    var value int

    getter := func() int {
        return value
    }

    setter := func(v int) {
        value = v
    }

    return getter, setter
}

func main() {
    get, set := makePair()

    fmt.Println(get())  // 0
    set(42)
    fmt.Println(get())  // 42
}

gettersetter 共享同一个 value 变量。

闭包陷阱

Go 1.22 之前的循环变量陷阱

在 Go 1.22 之前,循环变量在整个循环中只有一个,闭包捕获的是同一个变量:

// Go 1.21 的行为(有 bug)
funcs := []func(){}
for i := 0; i < 3; i++ {
    funcs = append(funcs, func() {
        fmt.Println(i)
    })
}
for _, f := range funcs {
    f()
}
// 输出:3 3 3(全部是循环结束后的值)

所有闭包捕获的是同一个 i,循环结束后 i 是 3,所以全部输出 3。

旧版的解决办法:

for i := 0; i < 3; i++ {
    i := i  // 创建局部副本
    funcs = append(funcs, func() {
        fmt.Println(i)
    })
}

Go 1.22+ 的修复

Go 1.22 开始,每次迭代创建新的变量,闭包捕获的是各自的 i

// Go 1.22+ 的行为(已修复)
funcs := []func(){}
for i := 0; i < 3; i++ {
    funcs = append(funcs, func() {
        fmt.Println(i)
    })
}
for _, f := range funcs {
    f()
}
// 输出:0 1 2
Note

这个修复在上一章「循环变量语义」里详细讲过。Go 1.22+ 你不需要再担心这个陷阱了。

goroutine 中的闭包

// Go 1.22+ 正确行为
for i := 0; i < 3; i++ {
    go func() {
        fmt.Println(i)
    }()
}
// 可能输出:0 1 2(顺序不确定)

Go 1.22 之前需要传参:

for i := 0; i < 3; i++ {
    go func(i int) {
        fmt.Println(i)
    }(i)
}

闭包的实际应用

回调函数

func fetchData(url string, onSuccess func(data string), onError func(err error)) {
    // 模拟异步操作
    if url == "" {
        onError(errors.New("URL 不能为空"))
        return
    }
    onSuccess("来自 " + url + " 的数据")
}

func main() {
    fetchData("https://example.com",
        func(data string) {
            fmt.Println("成功:", data)
        },
        func(err error) {
            fmt.Println("失败:", err)
        },
    )
}

延迟执行

func main() {
    msg := "Hello"

    // defer 注册一个匿名函数
    defer func() {
        fmt.Println(msg)  // 在 main 退出时执行
    }()

    msg = "World"
    fmt.Println("函数执行中")
}
// 输出:
// 函数执行中
// World
Tip

defer 的参数在注册时就求值,但闭包捕获的变量在执行时才读取。所以 msg 输出的是修改后的 “World”。defer 的细节下一节讲。

中间件模式

func logger(handler func(string)) func(string) {
    return func(msg string) {
        fmt.Println("[LOG] 开始处理")
        handler(msg)
        fmt.Println("[LOG] 处理完成")
    }
}

func main() {
    sayHello := func(name string) {
        fmt.Println("Hello,", name)
    }

    loggedHello := logger(sayHello)
    loggedHello("Go")
}
// 输出:
// [LOG] 开始处理
// Hello, Go
// [LOG] 处理完成

函数工厂

func makeAdder(base int) func(int) int {
    return func(x int) int {
        return x + base
    }
}

func main() {
    addTen := makeAdder(10)
    addTwenty := makeAdder(20)

    fmt.Println(addTen(5))     // 15
    fmt.Println(addTwenty(5))  // 25
}

每个闭包都有自己的 base,互不影响。

闭包和内存

闭包引用的外部变量不会被垃圾回收,只要闭包还在使用:

func makeCounter() func() int {
    count := 0  // 这个变量不会被 GC
    return func() int {
        count++
        return count
    }
}

只要返回的闭包还在被引用,count 就不会被回收。这是闭包的正常行为,但要注意不要意外持有大量数据。

常见问题

闭包修改外部变量

func main() {
    x := 10

    f := func() {
        x = 20  // 修改了外部的 x
    }

    f()
    fmt.Println(x)  // 20
}

闭包可以修改捕获的变量,不只是读取。

闭包返回值

func wrapper() func() int {
    x := 0
    return func() int {
        x++
        return x
    }
}

每次调用返回的函数,x 会累加。不同调用得到不同的 wrapper() 返回值,各自独立:

c1 := wrapper()
c2 := wrapper()

c1()  // 1
c1()  // 2
c2()  // 1(独立的计数器)
c1()  // 3

闭包 vs 全局变量

闭包比全局变量的好处是封装。外部无法直接访问闭包捕获的变量,只能通过闭包提供的接口操作:

// 闭包方式:count 被封装,只能通过 increment 修改
counter := makeCounter()

// 全局变量方式:count 可以被任何代码修改,不安全
var count int
func increment() { count++ }

学完这节你能做什么

  • 写匿名函数并立即执行或赋值给变量
  • 理解闭包捕获外部变量的机制
  • 用闭包实现计数器、函数工厂
  • 避免 Go 1.22 之前的循环变量闭包陷阱
  • 用闭包实现回调、中间件等模式

下一节讲 defer 语句。