闭包
什么是闭包?
一句话理解:闭包就是一个“自带背包”的函数
在普通的函数调用中,函数执行完,里面的变量就会被销毁(内存被释放)。
闭包(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 执行完后仍然存在!
}
引用还是复制?
重要概念:闭包捕获的是变量的引用,不是值的拷贝!
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)