Go语言基础知识点 —— Closure 闭包

什么是闭包?

闭包概念

什么是闭包?

有这么一个对闭包的总结: 闭包=函数+引用环境。因此闭包的核心就是:函数引用环境

个人理解是,闭包其实就是一个特殊函数,他可以捕获函数内部变量和参数,并将它们与函数创建的环境绑定在一起。这样,当函数外部引用这个闭包时,闭包就可以访问这些变量和参数了,并维护这个环境。

如下图:

image-20230721143504324

常见的闭包创建方式就是在一个函数内部创建另一个函数,通过另一个函数访问这个函数的局部变量。为了更加清楚闭包函数的实现,我们来通过一个实际代码例子来分析闭包:

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()
}

该代码是定义了一个函数 newCounternewCounter函数包含一个局部变量countnewCounter函数中还定义了一个匿名函数(我们且称为函数f),并返回f

那么当调用newCounter()时,newCounter函数执行完毕退出,但由于匿名函数f存储了newCounter中的变量count,所以count并没有销毁,而是被封装在了函数f中。因此,当你通过f()调用函数f时,它还可以访问和修改count

这就是闭包的特性,通过内部的函数的方式获取其所在函数的引用环境的变量和参数访问和修改权限。在本质上,闭包是将函数内部和函数外部连接起来的桥梁。

借用网上的一句通俗的比喻来总结这个特性,就是别人家有一颗果树,你想吃果子但是因为权限不够吃不到(看到吃不到),但是你可以跟家里的孩子套近乎,通过他拿到你想要的果子。这个 别人家 就是局部作用域,外部无法访问内部变量,孩子是返回对象(此处是匿名函数),对家里的 东西有访问权限,我们可以借助返回对象间接的访问内部变量。

因此,可以将闭包理解为函数以及函数所引用的自由变量的组合。函数提供了逻辑和操作的功能,而自由变量提供了函数执行时的上下文信息。这种组合使得闭包具有保持状态、记忆和灵活性的能力。如图:

image-20230721152136360

闭包产生条件

来总结下闭包产生的必要的几个条件:

  1. 在函数 A 内部直接或者间接返回一个函数 B
  2. B 函数内部使用着 A 函数的私有变量(私有数据)
  3. A 函数外部有一个变量接受着函数 B

闭包的优缺点

闭包是一种非常有用的编程概念,它在Go语言中被广泛应用。然而,闭包也有其优点和缺点,让我们总结一下:

优点:

  1. 延长了变量的生命周期: 闭包允许函数捕获和保存其外部作用域中的变量,形成一个封闭的环境,函数的执行空间不销毁, 变量也不会销毁。这使得函数可以在返回后继续访问和修改这些变量,实现了一种状态的封装。

  2. 保护私有变量: 通过闭包,我们可以访问函数内部的私有变量,同时也保护函数内部的私有变量不被外界访问。

  3. 延迟执行: 闭包可以用于延迟执行一些操作,使其在某个特定的时刻执行。这在需要在函数执行结束后再执行某些操作时非常有用,比如资源清理、释放等。

  4. 回调函数: 闭包可以用作回调函数,将特定行为传递给其他函数,使得代码更加灵活和可扩展。

缺点:

  1. 资源泄露: 闭包中捕获的外部变量可能导致资源泄露。如果闭包中的变量引用了大量内存或者文件资源,并且闭包被长时间保持活动状态,可能会导致资源无法及时释放,造成内存泄漏等问题。

  2. 性能损耗: 闭包的实现可能会导致一定的性能损耗。因为闭包需要在堆上分配内存来保存捕获的变量,相比普通函数,闭包可能会有更多的内存分配和垃圾回收开销。

  3. 代码可读性: 使用过度复杂的闭包可能会降低代码的可读性和可维护性。当闭包嵌套层次过深或过于复杂时,可能会导致代码难以理解和调试。

虽然闭包有一些潜在的问题,但在适当的情况下,合理地使用闭包可以提高代码的灵活性和可维护性,使得代码更加简洁和优雅。在编写代码时,我们应该根据具体的场景和需求来选择是否使用闭包。合理地使用闭包,可以使代码更加清晰、高效和易于维护。

闭包的应用

Go语言中闭包的应用场景非常广泛,常见的用途包括函数工厂、延迟执行、回调函数等。

下面给出几个闭包的应用场景以及相应的代码示例:

  1. 函数工厂(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 来创建多个独立的累加器。

  2. 延迟执行(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 返回的闭包函数会在后续的代码之前被执行。

  3. 回调函数(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是以下图所示形式存在:

image-20230724154401884

而闭包最常见的方式就是引用了其外层函数定义的局部变量并以函数方式返回。而这种方式也符合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)	
  ......

得到如下栈帧图:

image-20230725162633345

每个闭包对象都是一个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		
  ......

调用图示如下:

image-20230725164400998

如果是没有捕获列表的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/

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值