Go语言闭包研究

·  阅读 902

Go语言闭包研究

闭包是Go语言中一个非常抽象的概念,也是笔者在Go语言的学习过程中遇到的第一个难点,希望本文可以较为细致的将闭包的概念及特性介绍清楚。

从函数变量说起

在Go语言中,函数也是一个变量,有类型、有值、有地址、可以被赋值、引用等,函数的零值为nil,但函数之间不能进行比较运算。

什么是闭包

由于函数也是变量,自然的可以想到函数也是有地址的(对汇编和操作系统有一定了解后就会知道,每一次函数的调用都会在内存中为该函数分配一片区域,叫做栈帧),而闭包,就可以理解为将函数的地址赋值给了一个变量,又由于Go的变量会进行自动反向引用,所以可以将该变量也当作一个函数使用(且由于栈帧的概念,该变量中还保存着函数被赋值时的内部各个变量的状态)。下面给出例子对其进行展示:

package main

import "fmt"

func inc() func() int {	//返回值为函数闭包
    var x int
    return func() int {
        x++
        return x
    }
}

func main(){
    i := incr()	//i被赋值为函数闭包,i内保存了x的状态
    fmt.printLn(i())	// 1
    fmt.printLn(i())	// 2
    fmt.printLn(i())	// 3
    
    fmt.printLn(incr()())	//1
    fmt.printLn(incr()())	//1
    fmt.printLn(incr()())	//1
    
    return
}
复制代码

最开始的三个输出中,由于i内保存了x的状态,故每次对i的调用都会通过i内保存的指向x的指针修改x的值,且状态继续保存在i中,这种状态叫做x逃逸,它的生命周期没有随着作用域的结束而结束。

之后的三个输出时,由于每一次都调用一次incr()返回的一个闭包,故三次的x属于不同的栈帧,状态各自独立。

闭包引用

理解了闭包的定义之后,对于闭包的引用也就自然而然了。闭包的引用其实就和其他变量的引用一样,只不过引用闭包之后得到的是一个保存了闭包声明时的状态的函数。需要注意的是,闭包对于外层词法域变量是引用的,也即,在闭包外部的变量修改可能会影响闭包内部的值,通过以下例子进行说明:

x := 1
f := func() {
	println(x)
}()	//等价于定义之后调用一次f(), 输出1
x = 2
f()	// 2
x = 3
f()	// 3
复制代码

每次调用f时都会对x解引用取值,因为闭包内保存的是x的地址。

进阶实例

接下来给出一个较为复杂的例子,涉及到了闭包的循环引用,借此可以加深对闭包的特性的理解:

var funcSlice []func()
for i := 0; i < 3; i++ {
	funcSlice = append(funcSlice, func() {
		println(i)
	})
}
for j := 0; j < 3; j++ {
	funcSlice[j]()
}
复制代码

读者可以先思考一下这段代码最后会输出什么。

5

4

3

2

1

0

这段代码最后的输出结果为3 3 3

感觉有点不理解了?这是正常的,让我们分析一下这段代码。

首先是对funcSlice这个slice变量的声明,通过一个for循环,每次都在其末尾添加一个闭包,闭包内是将变量i输出。

看到这个闭包的定义,是不是与之前那个输出x的函数一样?所以这里的i也是一个引用,闭包内实际上保存着的是i的地址,只不过在调用闭包函数时,自动解引用取了其保存的值,可以这样修改这段代码:

var funcSlice []func()
for i := 0; i < 3; i++ {
	funcSlice = append(funcSlice, func() {
		println(&i)
	})
}
for j := 0; j < 3; j++ {
	funcSlice[j]()
}
复制代码

看到的输出结果为0xc0000ac1d0 0xc0000ac1d0 0xc0000ac1d0,即每一次声明这个闭包得到的i的地址是一样的,故最后i的输出结果都是第一个for循环结束后i被赋予的值3

那么如果要让输出结果为1 2 3有什么方法呢?下面给出两种解决方案,这两种解决方案也是遇到大多数闭包引用问题时可以使用的方法。

一、声明新变量

在返回的闭包内声明新变量:Output := i,并输出j,这样输出的就不再是对i的引用而是Output的值。修改后的代码为:

var funcSlice []func()
for i := 0; i < 3; i++ {
	funcSlice = append(funcSlice, func() {
        Output := i
		println(Output)
	})
}
for j := 0; j < 3; j++ {
	funcSlice[j]()
}
复制代码

二、声明新函数并传参

将代码修改为:

var funcSlice []func()
for i := 0; i < 3; i++ {
	func(i int) {
		funcSlice = append(funcSlice, func() {
			println(i)
		})	//闭包
	}(i)	//闭包并调用
}
for j := 0; j < 3; j++ {
	funcSlice[j]()	//调用
}
复制代码

现在 println(i) 使用的 i 是通过函数参数传递进来的,并且 Go 语言的函数参数是按值传递的。

所以相当于在这个新的匿名函数内声明了三个变量,被三个闭包函数独立引用。原理跟第一种方法是一样的。

思考题

希望通过本篇博客读者能对Go语言中的闭包有一定的掌握。

可以自行尝试使用闭包是实现一个Fibonacci函数,其返回值为一个闭包,能返回连续的Fibonacci数。

参考链接

Go语言闭包详解

分类:
阅读
标签:
收藏成功!
已添加到「」, 点击更改