Go语言闭包与匿名函数

在Go语言中,函数是“一等公民”,可以像其他值一样被传递和赋值。匿名函数是没有名称的函数,而闭包则是指匿名函数捕获其外部作用域变量的能力。闭包使得函数可以携带状态,成为函数式编程风格的重要工具。

核心概念:匿名函数可以直接调用、赋值给变量或作为参数/返回值;闭包会“记住”它引用的外部变量,即使外部函数已经返回。

1. 匿名函数定义与调用

匿名函数没有名字,通常直接在需要的地方定义并调用,或者赋值给变量。

定义并立即调用
package main

import "fmt"

func main() {
    // 立即执行的匿名函数
    func() {
        fmt.Println("匿名函数执行了")
    }()

    // 带参数的匿名函数
    func(msg string) {
        fmt.Println("消息:", msg)
    }("Hello Go")

    // 带返回值的匿名函数
    sum := func(a, b int) int {
        return a + b
    }(10, 20)
    fmt.Println("求和结果:", sum)
}

赋值给变量

将匿名函数赋值给变量,就可以多次调用。

add := func(a, b int) int {
    return a + b
}
result := add(5, 3)
fmt.Println(result) // 8

2. 匿名函数作为参数(回调)

将匿名函数传递给其他函数,实现回调或定制行为。

// 接收一个函数作为参数,对切片每个元素应用该函数
func mapSlice(s []int, fn func(int) int) []int {
    result := make([]int, len(s))
    for i, v := range s {
        result[i] = fn(v)
    }
    return result
}

func main() {
    nums := []int{1, 2, 3, 4}
    doubled := mapSlice(nums, func(x int) int {
        return x * 2
    })
    fmt.Println(doubled) // [2 4 6 8]
}

3. 匿名函数作为返回值:闭包

当一个函数返回一个匿名函数,并且该匿名函数引用了外部函数的局部变量,就形成了闭包。闭包中的变量会“存活”在匿名函数中,即使外部函数已经退出。

最简单的闭包示例
func counter() func() int {
    // 局部变量 count 被内部匿名函数捕获
    count := 0
    return func() int {
        count++
        return count
    }
}

func main() {
    c1 := counter()
    fmt.Println(c1()) // 1
    fmt.Println(c1()) // 2
    fmt.Println(c1()) // 3

    c2 := counter()  // 新的闭包,拥有独立的 count
    fmt.Println(c2()) // 1
}
每次调用counter()都会创建一个新的count变量,并返回一个引用了它的闭包。每个闭包都有自己的状态。

4. 闭包捕获的是变量的引用

闭包捕获的是外部变量的引用(而非副本),因此闭包内部可以修改变量,并且多个闭包可以共享同一个变量。

func accumulator() func(int) int {
    total := 0
    return func(inc int) int {
        total += inc
        return total
    }
}

func main() {
    acc := accumulator()
    fmt.Println(acc(10)) // 10
    fmt.Println(acc(20)) // 30
    fmt.Println(acc(5))  // 35
}

5. 常见陷阱:循环中捕获迭代变量

在循环中定义闭包时,所有闭包捕获的是同一个循环变量的地址,导致预期外的结果。

func main() {
    funcs := []func(){}
    for i := 0; i < 3; i++ {
        funcs = append(funcs, func() {
            fmt.Println(i) // 捕获的是变量 i 的引用
        })
    }
    for _, f := range funcs {
        f() // 输出 3 3 3(而不是 0 1 2)
    }
}

解决方法:在循环内创建一个新变量,或者通过参数传递。

for i := 0; i < 3; i++ {
    i := i // 创建新变量,遮蔽外层 i
    funcs = append(funcs, func() {
        fmt.Println(i)
    })
}
// 或通过参数传递
for i := 0; i < 3; i++ {
    funcs = append(funcs, func(n int) func() {
        return func() { fmt.Println(n) }
    }(i))
}
注意:这是Go语言中常见的陷阱,Go 1.22 版本开始对 for 循环语义进行了调整,每次迭代会创建新的循环变量实例,从而避免了此问题。但为了兼容旧版本,仍建议采用显式复制的方式。

6. 闭包应用场景

  • 数据隐藏(模拟私有变量):通过闭包封装状态,只暴露操作函数。
  • 函数工厂:根据不同参数生成不同行为的函数。
  • 延迟执行与资源管理:结合defer使用。
  • 函数式编程:如过滤、映射、归约等。

示例:函数工厂(生成乘法器)

func multiplyBy(factor int) func(int) int {
    return func(x int) int {
        return x * factor
    }
}
double := multiplyBy(2)
triple := multiplyBy(3)
fmt.Println(double(5)) // 10
fmt.Println(triple(5)) // 15

7. 闭包实现递归

匿名函数想要递归调用自身,需要先将其赋值给一个变量。

var fib func(n int) int
fib = func(n int) int {
    if n < 2 {
        return n
    }
    return fib(n-1) + fib(n-2)
}
fmt.Println(fib(10)) // 55

综合示例:使用闭包定制排序

通过闭包实现灵活排序
package main

import (
    "fmt"
    "sort"
)

type Person struct {
    Name string
    Age  int
}

func main() {
    people := []Person{
        {"Alice", 30},
        {"Bob", 25},
        {"Charlie", 35},
    }

    // 按年龄升序排序的闭包
    sort.Slice(people, func(i, j int) bool {
        return people[i].Age < people[j].Age
    })
    fmt.Println("按年龄排序:", people)

    // 按名字长度排序的闭包
    sort.Slice(people, func(i, j int) bool {
        return len(people[i].Name) < len(people[j].Name)
    })
    fmt.Println("按名字长度排序:", people)
}
按年龄排序: [{Bob 25} {Alice 30} {Charlie 35}]
按名字长度排序: [{Bob 25} {Alice 30} {Charlie 35}]
小结:匿名函数和闭包是Go语言函数式编程风格的基石。闭包让函数可以“记住”执行上下文,适用于数据隔离、函数工厂、回调等场景。正确理解闭包捕获变量的机制,可以避免常见的坑(如循环变量捕获)。下一章将介绍Go语言中的指针,进一步理解内存操作。