什么是闭包?
闭包概念
什么是闭包?
有这么一个对闭包的总结: 闭包=函数+引用环境。因此闭包的核心就是:函数和引用环境。
个人理解是,闭包其实就是一个特殊函数,他可以捕获函数内部变量和参数,并将它们与函数创建的环境绑定在一起。这样,当函数外部引用这个闭包时,闭包就可以访问这些变量和参数了,并维护这个环境。
如下图:
常见的闭包创建方式就是在一个函数内部创建另一个函数,通过另一个函数访问这个函数的局部变量。为了更加清楚闭包函数的实现,我们来通过一个实际代码例子来分析闭包:
package main
import "fmt"
func newCounter() func() int {
count := 0
return func() int {
count++
return count
}
}
func main() {
f := newCounter()
f() // 1
f() // 2
f1 := newCounter()
f1() // 1
f1()
}
该代码是定义了一个函数 newCounter
, newCounter
函数包含一个局部变量count
, newCounter
函数中还定义了一个匿名函数(我们且称为函数f
),并返回f
。
那么当调用newCounter()
时,newCounter
函数执行完毕退出,但由于匿名函数f
存储了newCounter
中的变量count
,所以count
并没有销毁,而是被封装在了函数f
中。因此,当你通过f()
调用函数f
时,它还可以访问和修改count
。
这就是闭包的特性,通过内部的函数的方式获取其所在函数的引用环境的变量和参数访问和修改权限。在本质上,闭包是将函数内部和函数外部连接起来的桥梁。
借用网上的一句通俗的比喻来总结这个特性,就是别人家有一颗果树,你想吃果子但是因为权限不够吃不到(看到吃不到),但是你可以跟家里的孩子套近乎,通过他拿到你想要的果子。这个 别人家 就是局部作用域,外部无法访问内部变量,孩子是返回对象(此处是匿名函数),对家里的 东西有访问权限,我们可以借助返回对象间接的访问内部变量。
因此,可以将闭包理解为函数以及函数所引用的自由变量的组合。函数提供了逻辑和操作的功能,而自由变量提供了函数执行时的上下文信息。这种组合使得闭包具有保持状态、记忆和灵活性的能力。如图:
闭包产生条件
来总结下闭包产生的必要的几个条件:
- 在函数
A
内部直接或者间接返回一个函数B
B
函数内部使用着A
函数的私有变量(私有数据)A
函数外部有一个变量接受着函数B
闭包的优缺点
闭包是一种非常有用的编程概念,它在Go
语言中被广泛应用。然而,闭包也有其优点和缺点,让我们总结一下:
优点:
-
延长了变量的生命周期: 闭包允许函数捕获和保存其外部作用域中的变量,形成一个封闭的环境,函数的执行空间不销毁, 变量也不会销毁。这使得函数可以在返回后继续访问和修改这些变量,实现了一种状态的封装。
-
保护私有变量: 通过闭包,我们可以访问函数内部的私有变量,同时也保护函数内部的私有变量不被外界访问。
-
延迟执行: 闭包可以用于延迟执行一些操作,使其在某个特定的时刻执行。这在需要在函数执行结束后再执行某些操作时非常有用,比如资源清理、释放等。
-
回调函数: 闭包可以用作回调函数,将特定行为传递给其他函数,使得代码更加灵活和可扩展。
缺点:
-
资源泄露: 闭包中捕获的外部变量可能导致资源泄露。如果闭包中的变量引用了大量内存或者文件资源,并且闭包被长时间保持活动状态,可能会导致资源无法及时释放,造成内存泄漏等问题。
-
性能损耗: 闭包的实现可能会导致一定的性能损耗。因为闭包需要在堆上分配内存来保存捕获的变量,相比普通函数,闭包可能会有更多的内存分配和垃圾回收开销。
-
代码可读性: 使用过度复杂的闭包可能会降低代码的可读性和可维护性。当闭包嵌套层次过深或过于复杂时,可能会导致代码难以理解和调试。
虽然闭包有一些潜在的问题,但在适当的情况下,合理地使用闭包可以提高代码的灵活性和可维护性,使得代码更加简洁和优雅。在编写代码时,我们应该根据具体的场景和需求来选择是否使用闭包。合理地使用闭包,可以使代码更加清晰、高效和易于维护。
闭包的应用
Go语言中闭包的应用场景非常广泛,常见的用途包括函数工厂、延迟执行、回调函数等。
下面给出几个闭包的应用场景以及相应的代码示例:
-
函数工厂(
Function Factory
):闭包可以用于生成一系列相关的函数。func addGenerator() func(int) int { sum := 0 return func(x int) int { sum += x return sum } } func main() { addFunc := addGenerator() fmt.Println(addFunc(1)) // 输出 1 fmt.Println(addFunc(2)) // 输出 3 fmt.Println(addFunc(3)) // 输出 6 }
在上面的例子中,
addGenerator
函数返回了一个闭包函数,该闭包函数可以对一个变量sum
进行累加操作。每次调用返回的闭包函数时,sum
的值都会保留,并在累加后返回结果。这样,我们可以通过addGenerator
来创建多个独立的累加器。 -
延迟执行(
Defer Execution
):闭包可以用于延迟执行一些操作func doLater(msg string) func() { return func() { fmt.Println("Later:", msg) } } func main() { msg := "Hello, World!" deferFunc := doLater(msg) defer deferFunc() fmt.Println("Doing something...") }
在上面的例子中,
doLater
函数返回了一个闭包函数,该闭包函数在被调用时会输出msg
变量的内容。我们将这个闭包函数通过defer
延迟执行,这样在函数执行结束后,doLater
返回的闭包函数会在后续的代码之前被执行。 -
回调函数(
Callback Function
):闭包可以用作回调函数,在某些条件满足时执行func forEach(numbers []int, callback func(int)) { for _, num := range numbers { callback(num) } } func main() { numbers := []int{1, 2, 3, 4, 5} forEach(numbers, func(num int) { fmt.Println("Number:", num) }) }
在上面的例子中,
forEach
函数接受一个整数切片和一个回调函数作为参数。forEach
函数会遍历切片中的每个元素,并将每个元素作为参数传递给回调函数。我们通过闭包的方式将打印每个元素的操作作为回调函数传递给forEach
函数,并在遍历过程中执行回调函数。
这些例子只是闭包在Go
语言中的一些简单应用,实际上,闭包的应用场景非常广泛,可以通过闭包实现更加复杂和灵活的功能。闭包使得Go语言支持了更强大的编程范式,更灵活地处理和传递函数。
Function Value
Go
语言中,函数是头等对象,将函数作为参数变量或返回值的情况称为function value
。
function value
本质上是一个指针,指向runtime.funcval
结构体,这个结构体里只有一个地址,即函数指令的入口地址。代码如下:
type funcval struct {
fn uintptr
// variable-size, fn-specific data here
}
这个结构体从定义上看只有一个地址,这个地址才是函数的指令入口。一个Function Value
是以下图所示形式存在:
而闭包最常见的方式就是引用了其外层函数定义的局部变量并以函数方式返回。而这种方式也符合function value
,所以在Go语言中闭包只是拥有一个或多个捕获变量的Function Value而已。
比如还是如下这个闭包例子:
package main
func newCounter() func() int {
count := 0
return func() int {
count++
return count
}
}
func main() {
f := newCounter()
f1 := newCounter()
f() //count:1
f1()//count:1
}
我们通过汇编代码,将该函数涉及的栈帧构造出来,主要汇编代码如下:
TEXT main.newCounter(SB) gofile../Users/xjx/workspace/golang/source_code_read/main.go
......
main.go:3 0x1297 4883ec30 SUBQ $0x30, SP
main.go:3 0x129b 48896c2428 MOVQ BP, 0x28(SP)
main.go:3 0x12a0 488d6c2428 LEAQ 0x28(SP), BP
main.go:3 0x12a5 48c744241000000000 MOVQ $0x0, 0x10(SP)
main.go:4 0x12ae 488d0500000000 LEAQ 0(IP), AX [3:7]R_PCREL:type:int
main.go:4 0x12b5 e800000000 CALL 0x12ba [1:5]R_CALL:runtime.newobject<1>
main.go:4 0x12ba 4889442420 MOVQ AX, 0x20(SP)
main.go:4 0x12bf 48c70000000000 MOVQ $0x0, 0(AX)
main.go:5 0x12c6 488d0500000000 LEAQ 0(IP), AX [3:7]R_PCREL:type:noalg.struct { F uintptr; main.count *int }
main.go:5 0x12cd e800000000 CALL 0x12d2 [1:5]R_CALL:runtime.newobject<1>
main.go:5 0x12d2 4889442418 MOVQ AX, 0x18(SP)
main.go:5 0x12d7 488d0d00000000 LEAQ 0(IP), CX [3:7]R_PCREL:main.newCounter.func1
main.go:5 0x12de 488908 MOVQ CX, 0(AX)
main.go:5 0x12e1 488b7c2418 MOVQ 0x18(SP), DI
main.go:5 0x12e6 8407 TESTB AL, 0(DI)
main.go:5 0x12e8 488b4c2420 MOVQ 0x20(SP), CX
main.go:5 0x12ed 488d5708 LEAQ 0x8(DI), DX
main.go:5 0x12f1 833d0000000000 CMPL $0x0, 0(IP) [2:6]R_PCREL:runtime.writeBarrier+-1
main.go:5 0x12f8 7402 JE 0x12fc
main.go:5 0x12fa eb06 JMP 0x1302
main.go:5 0x12fc 48894f08 MOVQ CX, 0x8(DI)
main.go:5 0x1300 eb0a JMP 0x130c
main.go:5 0x1302 4889d7 MOVQ DX, DI
main.go:5 0x1305 e800000000 CALL 0x130a [1:5]R_CALL:runtime.gcWriteBarrierCX
main.go:5 0x130a eb00 JMP 0x130c
main.go:5 0x130c 488b442418 MOVQ 0x18(SP), AX
main.go:5 0x1311 4889442410 MOVQ AX, 0x10(SP)
main.go:5 0x1316 488b6c2428 MOVQ 0x28(SP), BP
main.go:5 0x131b 4883c430 ADDQ $0x30, SP
main.go:5 0x131f c3 RET
......
TEXT main.main(SB) gofile../Users/xjx/workspace/golang/source_code_read/main.go
......
main.go:11 0x1368 4883ec18 SUBQ $0x18, SP
main.go:11 0x136c 48896c2410 MOVQ BP, 0x10(SP)
main.go:11 0x1371 488d6c2410 LEAQ 0x10(SP), BP
main.go:12 0x1376 e800000000 CALL 0x137b [1:5]R_CALL:main.newCounter
main.go:12 0x137b 4889442408 MOVQ AX, 0x8(SP)
main.go:13 0x1380 6690 NOPW
main.go:13 0x1382 e800000000 CALL 0x1387 [1:5]R_CALL:main.newCounter
main.go:13 0x1387 48890424 MOVQ AX, 0(SP)
......
得到如下栈帧图:
每个闭包对象都是一个Function Value
,但是各自持有自己的捕获列表,这也是称闭包为有状态的函数的原因。
通过FunctionValue
调用函数时,会把对应的funcval
结构体地址存入特定寄存器,例如amd64
平台使用的是DX
寄存器。
继续使用闭包的示例,通过f
调用闭包函数时,会把f
存储的funcval
结构体地址存入寄存器DX
,这样在闭包函数的指令中就可以通过这个寄存器存储的地址加上8
字节的偏移,就找到f
的捕获变量了。
汇编代码如下:
TEXT main.main(SB) gofile../Users/xjx/workspace/golang/source_code_read/main.go
......
main.go:14 0x138b 488b542408 MOVQ 0x8(SP), DX
main.go:14 0x1390 488b02 MOVQ 0(DX), AX
main.go:14 0x1393 ffd0 CALL AX [0:0]R_CALLIND
......
调用图示如下:
如果是没有捕获列表的Function Value
,直接忽略这个寄存器即可。通过这样的方式,Go
语言实现了对Function Value
的统一调用。
其他的细节就不细说了,更多细节可以参考文章: https://mp.weixin.qq.com/s/iFYkcLbNK5pOA37N7ToJ5Q,该文章已经讲的非常清楚了,再次就不再重复了。
总结
最后总结如下:
- 闭包是函数值和引用环境的组合。
- 闭包可以捕获包含它的函数作用域内的变量。
- 闭包可以在函数结束后继续访问和修改捕获的变量。
- 每次调用函数时都会创建一个新的闭包实例,每个闭包实例都有自己的引用环境和捕获的变量。
- 闭包可以用于实现状态的保持和共享,尤其在并发编程中很有用。
- 闭包的生命周期可能会比函数长,因为它可以在函数结束后仍然被其他部分引用和使用。
- 使用闭包时要注意避免出现不必要的内存泄漏,确保在不需要时释放对捕获变量的引用。
闭包在 Go
语言中被广泛使用,可以帮助我们编写更灵活和功能强大的代码。它们提供了一种方式来将数据和行为捆绑在一起,并且可以在需要时进行延迟计算或保持状态。了解闭包的概念和使用方法对于理解和编写 Go
语言中的函数式代码非常重要。
参考资料:
【 幼麟实验室】 https://mp.weixin.qq.com/s/iFYkcLbNK5pOA37N7ToJ5Q
【chatgpt】 https://chat.openai.com/