Skip to content

闭包

概念

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

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

常见的闭包创建方式就是在一个函数内部创建另一个函数,通过另一个函数访问这个函数的局部变量。

go
package main

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

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

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

产生条件

闭包产生的必要的几个条件:

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

优缺点

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

优点:

  1. 延长了变量的生命周期: 闭包允许函数捕获和保存其外部作用域中的变量,形成一个封闭的环境,函数的执行空间不销毁, 变量也不会销毁。这使得函数可以在返回后继续访问和修改这些变量,实现了一种状态的封装。
  2. 保护私有变量: 通过闭包,我们可以访问函数内部的私有变量,同时也保护函数内部的私有变量不被外界访问。
  3. 延迟执行: 闭包可以用于延迟执行一些操作,使其在某个特定的时刻执行。这在需要在函数执行结束后再执行某些操作时非常有用,比如资源清理、释放等。
  4. 回调函数: 闭包可以用作回调函数,将特定行为传递给其他函数,使得代码更加灵活和可扩展。

缺点:

  1. 资源泄露: 闭包中捕获的外部变量可能导致资源泄露。如果闭包中的变量引用了大量内存或者文件资源,并且闭包被长时间保持活动状态,可能会导致资源无法及时释放,造成内存泄漏等问题。
  2. 性能损耗: 闭包的实现可能会导致一定的性能损耗。因为闭包需要在堆上分配内存来保存捕获的变量,相比普通函数,闭包可能会有更多的内存分配和垃圾回收开销。
  3. 代码可读性: 使用过度复杂的闭包可能会降低代码的可读性和可维护性。当闭包嵌套层次过深或过于复杂时,可能会导致代码难以理解和调试。

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

应用

函数工厂

闭包可以用于生成一系列相关的函数。

go
package main

import "fmt"

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 来创建多个独立的累加器。

延迟执行

闭包可以用于延迟执行一些操作

go
package main

import "fmt"

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

回调函数

闭包可以用作回调函数,在某些条件满足时执行

go
package main

import "fmt"

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

// 输出
Number: 1
Number: 2
Number: 3
Number: 4
Number: 5

在上面的例子中,forEach 函数接受一个整数切片和一个回调函数作为参数。forEach 函数会遍历切片中的每个元素,并将每个元素作为参数传递给回调函数。我们通过闭包的方式将打印每个元素的操作作为回调函数传递给 forEach 函数,并在遍历过程中执行回调函数。

使用闭包调试

当您在分析和调试复杂的程序时,无数个函数在不同的代码文件中相互调用,如果这时候能够准确地知道哪个文件中的具体哪个函数正在执行,对于调试是十分有帮助的。您可以使用 runtimelog 包中的特殊函数来实现这样的功能。包 runtime 中的函数 Caller() 提供了相应的信息,因此可以在需要的时候实现一个 where() 闭包函数来打印函数执行的位置:

go
package main

import (
 "log"
 "runtime"
)

func main() {
 where := func() {
  _, file, line, _ := runtime.Caller(1)
  log.Printf("%s:%d", file, line)
 }
 where()
 // some code
 where()
 // some more code
 where()
}

// 输出
2023/12/29 09:46:22 C:/xx/xxx/main.go:13
2023/12/29 09:46:22 C:/xx/xxx/main.go:15
2023/12/29 09:46:22 C:/xx/xxx/main.go:17

通过设置 log 包中的 flag 参数来实现

go
func main() {
 where := func() {
  log.SetFlags(log.Llongfile)
  log.Print("")
 }
 where()
 // some code
 where()
 // some more code
 where()
}

总结

闭包在 Go 语言中被广泛使用,可以帮助我们编写更灵活和功能强大的代码。它们提供了一种方式来将数据和行为捆绑在一起,并且可以在需要时进行延迟计算或保持状态。

  • 闭包是函数值和引用环境的组合。
  • 闭包可以捕获包含它的函数作用域内的变量。
  • 闭包可以在函数结束后继续访问和修改捕获的变量。
  • 每次调用函数时都会创建一个新的闭包实例,每个闭包实例都有自己的引用环境和捕获的变量。
  • 闭包可以用于实现状态的保持和共享,尤其在并发编程中很有用。
  • 闭包的生命周期可能会比函数长,因为它可以在函数结束后仍然被其他部分引用和使用。
  • 使用闭包时要注意避免出现不必要的内存泄漏,确保在不需要时释放对捕获变量的引用。