Go 语言闭包详解

栏目: Go · 发布时间: 5年前

内容简介:什么是闭包?下面就来通过几个例子来说明 Go 语言中的闭包以及由闭包引用产生的问题。在说明闭包之前,先来了解一下什么是
Go 语言闭包详解
Go 语言闭包详解

什么是闭包? 闭包是由函数和与其相关的引用环境组合而成的实体。

下面就来通过几个例子来说明 Go 语言中的闭包以及由闭包引用产生的问题。

函数变量(函数值)

在说明闭包之前,先来了解一下什么是 函数变量

在 Go 语言中,函数被看作是 第一类值 ,这意味着函数像变量一样,有类型、有值,其他普通变量能做的事它也可以。

func square(x int) {
	println(x * x)
}
复制代码
  1. 直接调用: square(1)
  2. 把函数当成变量一样赋值: s := square ;接着可以调用这个函数变量: s(1) 注意:这里 square 后面没有圆括号,调用才有。
  • 调用 nil 的函数变量会导致 panic。
  • 函数变量的零值是 nil ,这意味着它可以跟 nil 比较,但两个函数变量之间不能比较。

闭包

现在开始通过例子来说明闭包:

func incr() func() int {
	var x int
	return func() int {
		x++
		return x
	}
}
复制代码

调用这个函数会返回一个函数变量。

i := incr() :通过把这个函数变量赋值给 ii 就成为了一个 闭包

所以 i 保存着对 x 的引用,可以想象 i 中有着一个指针指向 xi 中有 x 的地址

由于 i 有着指向 x 的指针,所以可以修改 x ,且保持着状态:

println(i()) // 1
println(i()) // 2
println(i()) // 3
复制代码

也就是说, x 逃逸了,它的生命周期没有随着它的作用域结束而结束。

但是这段代码却不会递增:

println(incr()()) // 1
println(incr()()) // 1
println(incr()()) // 1
复制代码

这是因为这里调用了三次 incr() ,返回了三个闭包,这三个闭包引用着三个不同的 x ,它们的状态是各自独立的。

闭包引用

现在开始通过例子来说明由闭包引用产生的问题:

x := 1
f := func() {
	println(x)
}
x = 2
x = 3
f() // 3
复制代码

因为闭包对外层词法域变量是 引用 的,所以这段代码会输出 3

可以想象 f 中保存着 x 的地址,它使用 x 时会直接解引用,所以 x 的值改变了会导致 f 解引用得到的值也会改变。

但是,这段代码却会输出 1

x := 1
func() {
	println(x) // 1
}()
x = 2
x = 3
复制代码

把它转换成这样的形式就容易理解了:

x := 1
f := func() {
	println(x)
}
f() // 1
x = 2
x = 3
复制代码

这是因为 f 调用时就已经解引用取值了,这之后的修改就与它无关了。

不过如果再次调用 f 还是会输出 3 ,这也再一次证明了 f 中保存着 x 的地址。

可以通过在闭包内外打印所引用变量的地址来证明:

x := 1
func() {
	println(&x) // 0xc0000de790
}()
println(&x) // 0xc0000de790
复制代码

可以看到引用的是同一个地址。

循环闭包引用

接下来在三个例子中说明由循环内的闭包引用所产生的问题:

第一个例子

for i := 0; i < 3; i++ {
	func() {
		println(i) // 0, 1, 2
	}()
}
复制代码

这段代码相当于:

for i := 0; i < 3; i++ {
	f := func() {
		println(i) // 0, 1, 2
	}
	f()
}
复制代码

每次迭代后都对 i 进行了解引用并使用得到的值且不再使用,所以这段代码会正常输出。

第二个例子

正常代码:输出 0, 1, 2

var dummy [3]int
for i := 0; i < len(dummy); i++ {
	println(i) // 0, 1, 2
}
复制代码

然而这段代码会输出 3

var dummy [3]int
var f func()
for i := 0; i < len(dummy); i++ {
	f = func() {
		println(i)
	}
}
f() // 3
复制代码

前面讲到闭包取引用,所以这段代码应该输出 i 最后的值 2 对吧?

不对。这是因为 i 最后的值并不是 2

把循环转换成这样的形式就容易理解了:

var dummy [3]int
var f func()
for i := 0; i < len(dummy); {
	f = func() {
		println(i)
	}
	i++
}
f() // 3
复制代码

i 自加到 3 才会跳出循环,所以循环结束后 i 最后的值为 3

所以用 for range 来实现这个例子就不会这样:

var dummy [3]int
var f func()
for i := range dummy {
	f = func() {
		println(i)
	}
}
f() // 2
复制代码

这是因为 for rangefor 底层实现上的不同。

第三个例子

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

}
for j := 0; j < 3; j++ {
	funcSlice[j]() // 3, 3, 3
}
复制代码

输出序列为 3, 3, 3

看了前面的例子之后这里就容易理解了: 这三个函数引用的都是同一个变量( i )的地址,所以之后 i 递增,解引用得到的值也会递增,所以这三个函数都会输出 3

添加输出地址的代码可以证明:

var funcSlice []func()
for i := 0; i < 3; i++ {
	println(&i) // 0xc0000ac1d0 0xc0000ac1d0 0xc0000ac1d0
	funcSlice = append(funcSlice, func() {
		println(&i)
	})

}
for j := 0; j < 3; j++ {
	funcSlice[j]() // 0xc0000ac1d0 0xc0000ac1d0 0xc0000ac1d0
}
复制代码

可以看到三个函数引用的都是 i 的地址。

解决方法

1. 声明新变量:

  • 声明新变量: j := i ,且把之后对 i 的操作改为对 j 操作。
  • 声明新同名变量: i := i 注意:这里短声明右边是外层作用域的 i ,左边是新声明的作用域在这一层的 i 。原理同上。

这相当于为这三个函数各声明一个变量,一共三个,这三个变量初始值分别对应循环中的 i 并且之后不会再改变。

2. 声明新匿名函数并传参:

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]() // 0, 1, 2
}
复制代码

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

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

这里的解决方法可以用在大多数跟闭包引用有关的问题上,不局限于第三个例子。


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Algorithms

Algorithms

Robert Sedgewick、Kevin Wayne / Addison-Wesley Professional / 2011-3-19 / USD 89.99

Essential Information about Algorithms and Data Structures A Classic Reference The latest version of Sedgewick,s best-selling series, reflecting an indispensable body of knowledge developed over the ......一起来看看 《Algorithms》 这本书的介绍吧!

JS 压缩/解压工具
JS 压缩/解压工具

在线压缩/解压 JS 代码

SHA 加密
SHA 加密

SHA 加密工具

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换