欢迎,来自IP地址为:216.73.216.157 的朋友
如果 有过 JavaScript、Python 或者 Rust 的编程经验,那么对于闭包这个概念并不会陌生。虽然这个概念对于每种编程语言来说有些细微差异,但是核心理念是一致的:闭包是一个能访问其定义时所在的作用域中的变量的函数。这使得函数能够”记住”它被创建的环境,即使它在该环境之外执行,这对如何编写和构建代码具有重要的意义。
在本文中,我们将探讨 Go 语言(一种以简洁高效著称的静态类型语言)中闭包的工作原理。我们将了解如何创建闭包、闭包如何捕获变量以及一些实际用例。
1. Go 中的闭包到底是什么
简单来说,Go 中的闭包是一个引用在其外部定义的变量的函数。这听起来可能很抽象,那么让我们从一个可以立即运行的示例开始:
package main
import "fmt"
func counter() func() int {
n := 0
return func() int {
n++
return n
}
}
func main() {
next := counter()
fmt.Println(next()) // 1
fmt.Println(next()) // 2
fmt.Println(next()) // 3
}
当程序调用 counter() 函数时,会返回另一个函数,但这个函数会保持对于变量 n 的访问。即使是 counter() 函数执行完毕,变量 n 仍然不会消失。每次调用根据 counter() 函数生成的 next() 函数时,都会访问同一个变量 n,也就是初次调用时生成的那个。
这正是闭包的定义属性:
闭包”封闭”其环境,只要闭包本身存在,它所需的变量就会一直保持活动状态。
2. Go 如何实现闭包
通常,Go 中的局部变量存储于栈中,函数返回时栈会被清除。
但是,如果嵌套函数需要继续使用其中一个变量,Go 的编译器会执行所谓的逃逸分析:它会发现该变量的存活时间比函数调用的时间长,因此会将该变量移至堆,只要有引用它(在本例中是闭包),它就可以一直存活。
我们可以让编译器演示这个过程:
go build -gcflags="-m" main.go
示例输出如下:

从图中可以看出,Go 将变量 n 从栈移至堆中,稍后的闭包就可以安全的使用它。
多个独立闭包
每次调用返回闭包的函数都会创建一个新的、独立的环境:
a := counter() b := counter() fmt.Println(a()) // 1 fmt.Println(a()) // 2 fmt.Println(b()) // 1
这里,a 和 b 是两个独立的闭包,每个闭包都有各自的 n。调用 a() 会递增其自身的 n,而调用 b() 则会从其各自的 n 开始。
3. Go 创建闭包
在 Go 中创建闭包的方法有很多种。让我们来探讨一些常见的模式。
从函数返回闭包
最常见的模式是让函数返回一个闭包,该闭包维护自身的状态:
package main
import "fmt"
func makeCounter() func() int {
n := 0
return func() int {
n++
return n
}
}
func main() {
c1 := makeCounter()
fmt.Println(c1()) // 1
fmt.Println(c1()) // 2
}
每次调用 makeCounter() 函数都会创建一个具有自己 n 的新闭包,正如我们之前看到的那样。
命名内部函数
为了提高代码可读性或便于调试,还可以给函数字面量命名:
func makeCounter() func() int {
n := 0
next := func incr() int {
n++
return n
}
return next
}
这种方法与匿名函数的工作方式相同,但它会给内部函数赋予一个名称(incr),这在堆栈跟踪中非常有用。除此之外,它的行为与匿名函数完全相同。
循环或 goroutine 中的内联闭包
闭包通常以内联方式定义,尤其是在循环或 goroutine 中:
for i := 0; i < 3; i++ {
go func(x int) {
fmt.Println(x)
}(i)
}
在这里,我们将 i 作为参数传递给闭包,确保每个 goroutine 都获得自己的值副本,避免循环变量陷阱。
带参数的闭包
闭包可以接受自己的参数:
func adder(base int) func(int) int {
return func(x int) int {
return base + x
}
}
add5 := adder(5)
fmt.Println(add5(10)) // 15
在这里,adder 返回一个闭包,该闭包会将一个固定的基值加到它接收到的任何参数上。
捕获多个变量
闭包可以捕获多个外部变量:
func multiplier(factor int) func(int) int {
offset := 2
return func(x int) int {
return x*factor + offset
}
}
m := multiplier(3)
fmt.Println(m(4)) // 14
在这个例子中,闭包从其周围范围捕获了 factor 和 offset — factor 是一个参数,而 offset 是一个局部变量。
结构体中的闭包
闭包也可以像其他函数值一样存储在结构体中。当需要具有动态或有状态行为的对象时,这是一种非常有用的模式。
type Counter struct {
Next func() int
}
func NewCounter() Counter {
n := 0
return Counter{
Next: func() int {
n++
return n
},
}
}
func main() {
c := NewCounter()
fmt.Println(c.Next()) // 1
fmt.Println(c.Next()) // 2
}
这里,”Next”字段包含一个闭包,用于捕获变量 “n”。”Counter”的每个实例都有其独立的状态,无需单独的类型或互斥锁。
这种模式展示了闭包如何作为轻量级对象:将行为和状态捆绑在一起。
关于方法接收器的说明
Go 语言中的闭包不会像某些语言那样隐式地捕获方法接收器。如果希望闭包在方法内部使用接收器,通常需要将其赋值给一个局部变量:
type Counter struct {
n int
}
func (c *Counter) MakeIncrementer() func() int {
r := c // capture receiver explicitly
return func() int {
r.n++
return r.n
}
}
这样可以确保闭包引用的是预期的接收者,而不是引入意外行为。
与 JavaScript 或 Python 不同,Go 闭包捕获的是词法变量,而不是隐式的 “this” 或 “self”。
总结来说:
- 闭包可以从函数返回,可以命名、内联,甚至可以存储在结构体中
- 它们捕获外部变量,而不是其值的副本
- 这样使用时,闭包可以替代小型类型或接口,实现轻量级封装
4. 闭包与并发
闭包在 Go 语言中功能强大,但当它们与并发结合使用时,如果不小心,它们捕获的变量可能会出现意想不到的行为。
跨 goroutine 的独立状态
每个闭包都会保持其捕获的变量处于活动状态,即使在并发 goroutine 中使用也是如此:
package main
import "fmt"
func makeWorker(start int) func() int {
counter := start
return func() int {
counter++
return counter
}
}
func main() {
worker1 := makeWorker(0)
worker2 := makeWorker(100)
go func() { fmt.Println(worker1()) }() // prints 1
go func() { fmt.Println(worker2()) }() // prints 101
}
这里,worker1 和 worker2 拥有独立的计数器变量,因此它们互不干扰。每个闭包都维护着独立的状态,即使在不同的 goroutine 中也是如此。
安全地捕获共享变量
当多个闭包共享一个变量时,必须协调访问权限。例如:
package main
import "fmt"
func main() {
counter := 0
ch := make(chan int)
for i := 0; i < 3; i++ {
go func() {
// increments a shared variable
ch <- 1
}()
}
// aggregate safely
for i := 0; i < 3; i++ {
counter += <-ch
}
fmt.Println(counter) // 3
}
闭包捕获了外部变量 ch(一个通道),这是安全的,因为通道会串行化访问。在这里使用缓冲通道不会改变闭包的行为:它仍然会捕获自身的 n 并将值独立地发送到通道。
闭包本身并不同步共享状态,但仍然需要通道或互斥锁。
Go 语言中的闭包允许函数携带状态、封装行为,并安全地与并发模式交互,同时保持代码的简洁性和表达力。通过理解闭包如何捕获变量、它们在循环和 goroutine 中的行为以及它们的内存影响,就可以自信地使用它们来编写更符合 Go 语言习惯且更易于维护的代码。
