Golang闭包
侧边栏壁纸
  • 累计撰写 11 篇文章
  • 累计收到 1 条评论

Golang闭包

tqtqtq
2026-02-04 / 0 评论 / 10 阅读 / 正在检测是否收录...

闭包

什么是闭包?

一句话理解:闭包就是一个“自带背包”函数

在普通的函数调用中,函数执行完,里面的变量就会被销毁(内存被释放)。

闭包(Closure)是函数与其相关的引用环境组合而成的实体

简单说,闭包是一个函数,它能"记住"并访问创建它的上下文中的变量,即使这个函数在其原始作用域之外执行。

闭包 = 函数 + 引用环境

更准确地说:

  • 闭包是一个函数
  • 这个函数可以访问其外部函数作用域中的变量
  • 即使外部函数已经执行完毕,闭包仍然可以访问这些变量

在 Go 中,当一个匿名函数引用了其外部作用域的变量时,就形成了闭包。


代码拆解

为了看懂闭包,我们需要先理解 Go 语言中函数的两个特权:

  • 函数可以定义在另一个函数里面(匿名函数)。
  • 函数可以作为返回值返回。

第一步:看一个简单的计数器需求

如果我们要写一个计数器,每调用一次就加 1。

普通写法(使用全局变量):

var i int = 0 // 全局变量

func count() int {
    i++
    return i
}
// 缺点:i 是全局的,谁都能改它,不安全。

闭包写法(保护变量):

闭包的“标准模板”:

// accumulation 是一个函数,它返回值的类型是一个 func() int
func accumulator() func() int {
    // 1. 定义一个“外部”变量
    sum := 0

    // 2. 定义一个内部匿名函数,并返回它
    return func() int {
        // 3. 这个内部函数引用了外部的 sum
        sum++ 
        return sum
    }
}

func main() {
    // 创建一个闭包实例,赋值给变量 f
    f := accumulator()

    fmt.Println(f()) // 输出: 1
    fmt.Println(f()) // 输出: 2
    fmt.Println(f()) // 输出: 3
}

func accumulator() func() int: 外层函数。它不直接返回数字,而是返回一个动作(另一个函数)。

sum := 0: 这是定义在外层环境的变量。按理说 accumulator 执行完,sum 应该销毁。

return func() int {...}: 我们返回了内部函数。关键点来了: 这个内部函数在使用 sum

捕获(Capture): Go 编译器发现内部函数要用 sum,也是内部函数被返回到了外面。编译器会判定:“sum不能销毁,因为返回出去的那个东西还要用它”。

于是,sum 就被装进了内部函数的“背包”里,一起带出了 accumulator 函数。


闭包的原理

作用域的概念

在理解闭包之前,我们需要了解作用域:

package main

import "fmt"

var global = "我是全局变量" // 全局作用域

func outer() {
    outerVar := "我是外部函数变量" // outer 函数作用域
    
    func inner() {
        innerVar := "我是内部函数变量" // inner 函数作用域
        
        // inner 可以访问:
        fmt.Println(innerVar)   // 自己的变量
        fmt.Println(outerVar)   // 外部函数的变量
        fmt.Println(global)     // 全局变量
    }
    
    inner()
    // fmt.Println(innerVar) // 错误! outer 访问不了 inner 的变量
}

func main() {
    outer()
}

闭包如何"捕获"变量

package main

import "fmt"

func createCounter() func() int {
    count := 0 // 这个变量会被"捕获"
    
    // 返回的这个匿名函数形成了闭包
    return func() int {
        count++ // 访问并修改被捕获的变量
        return count
    }
}

func main() {
    counter := createCounter()
    
    fmt.Println(counter()) // 1
    fmt.Println(counter()) // 2
    fmt.Println(counter()) // 3
    
    // count 变量在 createCounter 执行完后仍然存在!
}

image-20260204113957645

引用还是复制?

重要概念:闭包捕获的是变量的引用,不是值的拷贝!

package main

import "fmt"

func main() {
    x := 10
    
    // 闭包捕获的是 x 的引用
    increment := func() {
        x++ // 修改的是同一个 x
    }
    
    fmt.Println("x =", x) // 10
    increment()
    fmt.Println("x =", x) // 11
    increment()
    fmt.Println("x =", x) // 12
    
    // x 被修改了!
}

基础用法

1. 捕获并修改外部变量

闭包最基本的特性就是可以访问并修改其外部作用域的变量,因为闭包持有的是变量的引用,而不是值的拷贝,所以修改会影响原变量。

func main() {
    x := 10
    
    // 闭包函数引用外部变量x
    increment := func() int {
        x++
        return x
    }
    
    fmt.Println(increment()) // 11
    fmt.Println(increment()) // 12
    fmt.Println(x)           // 12
}

2. 函数工厂模式

闭包常用于创建定制化的函数,每个闭包都维护自己的状态。

可以用来创建具有不同配置的相似函数,避免重复代码。

函数工厂(Function Factory):通过一个工厂函数,根据不同的配置参数,批量生产具有特定行为的功能函数。

// 工厂函数模板
func makeXXX(config) func(...) ... {
    // 配置被"封装"在闭包中
    return func(...) ... {
        // 使用配置执行操作
    }
}

e.g.简单案例——乘数工厂

package main

import "fmt"

// makeMultiplier 是工厂,factor是配置,返回的函数是产品
func makeMultiplier(factor int) func(int) int {
    // factor被闭包捕获,每个返回的函数"记住"自己的factor
    return func(x int) int {
        return x * factor
    }
}

func main() {
    // 生产不同配置的乘数器
    double := makeMultiplier(2)
    triple := makeMultiplier(3)
    tenfold := makeMultiplier(10)
    
    // 每个产品独立运行,互不干扰
    fmt.Println(double(5))   // 10
    fmt.Println(triple(5))  // 15
    fmt.Println(tenfold(5)) // 50
    
    // 验证状态隔离:修改一个不影响其他
    double2 := makeMultiplier(2)
    fmt.Println(double(100))  // 200
    fmt.Println(double2(100)) // 200 (相同配置,独立实例)
}

每次调用makeMultiplier,都会创建一个新的factor变量副本,被返回的闭包专属持有。

案例场景:配置化HTTP客户端

package main

import (
    "fmt"
    "time"
)

// HTTPClient 配置
type HTTPConfig struct {
    BaseURL    string
    Timeout    time.Duration
    RetryCount int
    Headers    map[string]string
}

// 工厂函数:根据配置创建专用的请求函数
func makeHTTPRequester(config HTTPConfig) func(endpoint string) string {
    // 预加载配置(只解析一次)
    fullBase := config.BaseURL
    if !endsWithSlash(fullBase) {
        fullBase += "/"
    }
    
    return func(endpoint string) string {
        // 闭包内使用预处理的配置
        url := fullBase + endpoint
        
        // 模拟请求逻辑
        result := fmt.Sprintf("请求: %s (超时:%v, 重试:%d次)",
            url, config.Timeout, config.RetryCount)
        return result
    }
}

func endsWithSlash(s string) bool {
    return len(s) > 0 && s[len(s)-1] == '/'
}

func main() {
    // 生产环境客户端
    prodClient := makeHTTPRequester(HTTPConfig{
        BaseURL:    "https://api.example.com",
        Timeout:    5 * time.Second,
        RetryCount: 3,
    })
    
    // 测试环境客户端
    testClient := makeHTTPRequester(HTTPConfig{
        BaseURL:    "http://localhost:8080",
        Timeout:    30 * time.Second,
        RetryCount: 0,
    })
    
    // 使用客户端(配置已内嵌,调用简洁)
    fmt.Println(prodClient("users/1"))
    fmt.Println(testClient("users/1"))
    fmt.Println(prodClient("orders/list"))
}

输出:

请求: https://api.example.com/users/1 (超时:5s, 重试:3次)
请求: http://localhost:8080/users/1 (超时:30s, 重试:0次)
请求: https://api.example.com/orders/list (超时:5s, 重试:3次)

闭包陷阱

1.循环变量陷阱(注意Go 1.2.2+有所不同)

package main

import (
    "fmt"
    "time"
)

func main() {
    // 错误示例
    for i := 0; i < 5; i++ {
        go func() {
            fmt.Println(i)  // 所有goroutine共享同一个i
        }()
    }
    time.Sleep(time.Second)
}
// 输出:可能全是 5 5 5 5 5

解析:所有 goroutine 捕获的是同一个变量 i 的引用,而不是值。当 goroutine 执行时,循环可能已经结束,i 的值变成了 5。

问题核心:在 Go 1.22 版本之前,for 循环中的变量在内存中只有一份,而不是每次循环都生成一个新的。

解决:

// 方法1: 传参
for i := 0; i < 5; i++ {
    go func(n int) {
        fmt.Println(n)
    }(i)  // 将i的当前值传入
}

// 方法2: 创建局部副本
for i := 0; i < 5; i++ {
    i := i  // 创建新的局部变量
    go func() {
        fmt.Println(i)
    }()
}

// 方法3: Go 1.22+ 版本自动修复
// 从 Go 1.22 开始,for 循环会为每次迭代创建新变量

2.延迟函数中的闭包陷阱

func main() {
    x := 10
    
    // 写法1: 传参
    defer func(val int) {
        fmt.Println("A:", val)
    }(x)

    // 写法2: 闭包捕获
    defer func() {
        fmt.Println("B:", x)
    }()

    x = 20
}

//输出:
//B: 20
//A: 10

解析:

写法1 (传参): defer声明时就会对函数的参数进行求值(Evaluate)。当时 x 是 10,所以 10 被拷贝进去了。

写法2 (闭包): defer 在声明时只记录了要执行函数,函数内部引用的 x 依然是对外部变量的引用。当函数在 main 结束前真正执行时x 已经被修改为 20。

常见的混淆点:求值时机。

0

评论 (0)

取消