Go语言中闭包是引用了自由变量的函数,被引用的自由变量和函数一同存在,即使已经离开了自由变量的环境也不会被释放或者删除,在闭包中可以继续使用这个自由变量,因此,简单的说:
闭包 = 函数 + 引用环境
一个函数类型就像结构体一样,可以被实例化,函数本身不存储任何信息,只有与引用环境结合后形成的闭包才具有“记忆性”,函数是编译期静态的概念,而闭包是运行期动态的概念。
函数在Golang中是“一等公民”,因此关于函数的特性必须要掌握好,闭包可以看成函数的高阶应用,是Golang高级开发的必备技能。
匿名函数
“一等公民”意味着函数可以像普通的类型(整型、字符串等)一样进行赋值、作为函数的参数传递、作为函数的返回值等。Golang的函数只能返回匿名函数!
var f = func(int) {} func main() { f = func(i int) { fmt.Println(i) } f(2) f = func(i int) { fmt.Println(i * i * i) } f(2) } /* 输出: 2 8 */
上述代码中,f
可以被任何输入一个整型,无返回值的函数给赋值,这类似于C++中的函数指针。因此f
可以看成是一个函数类型的变量。这样,可以动态的改变f
的功能。匿名函数可以动态的创建,与之成对比的常规函数必须在包中编译前就定义完毕。匿名函数可以随时改变功能。
闭包
闭包是匿名函数与匿名函数所引用环境的组合。匿名函数有动态创建的特性,该特性使得匿名函数不用通过参数传递的方式,就可以直接引用外部的变量。这就类似于常规函数直接使用全局变量一样,个人理解为:匿名函数和它引用的变量以及环境,类似常规函数引用全局变量处于一个包的环境。
func main() { n := 0 f := func() int { n += 1 return n } fmt.Println(f()) // 别忘记括号,不加括号相当于地址 fmt.Println(f()) } /* 输出: 1 2 */
在上述代码中,
n := 0 f := func() int { n += 1 return n }
就是一个闭包,类比于常规函数+全局变量+包。f
不仅仅是存储了一个函数的返回值,它同时存储了一个闭包的状态。
闭包作为函数返回值
匿名函数作为返回值,不如理解理解为闭包作为函数的返回值,如下代码:
func Increase() func() int { n := 0 return func() int { n++ return n } } func main() { in := Increase() fmt.Println(in()) fmt.Println(in()) } /* 输出: 1 2 */
闭包被返回赋予一个同类型的变量时,同时赋值的是整个闭包的状态,该状态会一直存在外部被赋值的变量in
中,直到in
被销毁,整个闭包也被销毁。
Golang并发中的闭包
Go语言的并发时,一定要处理好循环中的闭包引用的外部变量。如下代码:
func main() { runtime.GOMAXPROCS(runtime.NumCPU()) var wg sync.WaitGroup for i := 0; i < 5; i++ { wg.Add(1) go func() { fmt.Println(i) wg.Done() }() } wg.Wait() } 输出结果: 5 5 5 5 5
这种现象的原因在于闭包共享外部的变量i,注意到,每次调用go就会启动一个goroutine,这需要一定时间;但是,启动的goroutine与循环变量递增不是在同一个goroutine,可以把i认为处于主goroutine中。启动一个goroutine的速度远小于循环执行的速度,所以即使是第一个goroutine刚起启动时,外层的循环也执行到了最后一步了。由于所有的goroutine共享i,而且这个i会在最后一个使用它的goroutine结束后被销毁,所以最后的输出结果都是最后一步的i==5。
我们可以使用循环的延时在验证上述说法:
func main() { runtime.GOMAXPROCS(runtime.NumCPU()) var wg sync.WaitGroup for i := 0; i < 5; i++ { wg.Add(1) go func() { fmt.Println(i) wg.Done() }() time.Sleep(1 * time.Second) // 设置时间延时1秒 } wg.Wait() } /* 输出结果: 0 1 2 3 4 */
每一步循环至少间隔一秒,而这一秒的时间足够启动一个goroutine
了,因此这样可以输出正确的结果。
在实际的工程中,不可能进行延时,这样就没有并发的优势,一般采取下面两种方法:
- 共享的环境变量作为函数参数传递:
func main() { runtime.GOMAXPROCS(runtime.NumCPU()) var wg sync.WaitGroup for i := 0; i < 5; i++ { wg.Add(1) go func(i int) { fmt.Println(i) wg.Done() }(i) } wg.Wait() } /* 输出: 4 0 3 1 2 */
输出结果不一定按照顺序,这取决于每个goroutine
的实际情况,但是最后的结果是不变的。可以理解为,函数参数的传递是瞬时的,而且是在一个goroutine
执行之前就完成,所以此时执行的闭包存储了当前i
的状态。
2.使用同名的变量保留当前的状态
func main() { runtime.GOMAXPROCS(runtime.NumCPU()) var wg sync.WaitGroup for i := 0; i < 5; i++ { wg.Add(1) i := i // 注意这里的同名变量覆盖 go func() { fmt.Println(i) wg.Done() }() } wg.Wait() } /* 输出结果: 4 2 0 3 1 结果顺序原因同1 */
同名的变量i
作为内部的局部变量,覆盖了原来循环中的i
,此时闭包中的变量不在是共享外循环的i
,而是都有各自的内部同名变量i
,赋值过程发生于循环goroutine
,因此保证了独立。