1.Golang中的闭包
1.1.什么是闭包
当一个函数引用了环境的变量,被引用的变量与该函数同时存在组成的系统,被称为闭包。
闭包 = 环境的变量 + 函数。
以下JavaScript代码展示了一个基础的闭包:
- name是init函数中的内部变量
- displayName()是init函数中定义的函数
- displayName()函数引用了name变量
- displayName() = name + func() = 环境的变量 + 函数,所以displayName是一个引用了name变量的闭包
function init() {
var name = "something";
function displayName() {
alert(name);
}
displayName();
}
init();
1.2.Golang中使用闭包
Golang中天然支持闭包(函数是一等公民),下面看一个简单的case:
func someMethod() {
// 准备一个字符串
str := "hello world"
// 创建一个匿名函数
foo := func() {
// 匿名函数中访问str
str = "hello dude"
}
// 调用匿名函数
foo()
}
- 第3行,准备一个字符串用于修改
- 第5行,创建一个匿名函数
- 第8行,在匿名函数中并没有定义 str,str 的定义在匿名函数之前,此时,str 就被引用到了匿名函数中形成了闭包
- 第10行,执行闭包,此时 str 发生修改,变为 hello dude。
1.3.闭包的特性
1.3.1.记忆特性(环境绑定)
闭包可以记忆创建时的环境,并且一直保持对该环境的绑定,直到闭包被销毁。
一个累加器的例子:
// 提供一个值, 每次调用函数会指定对值进行累加
func Accumulate(value int) func() int {
return func() int {
value++
return value
}
}
func testAccumulator() {
// 创建一个累加器, 初始值为1
accumulator := Accumulate(1)
fmt.Printf("%p\n", &accumulator) // 0xc00000e028
fmt.Println(accumulator()) // 2
fmt.Println(accumulator()) // 3
// 创建一个累加器, 初始值为10
accumulator2 := Accumulate(10)
fmt.Printf("%p\n", &accumulator2) // 0xc00000e038
fmt.Println(accumulator2()) // 11
}
- Accumulate函数会创建一个绑定value值的闭包
- 第一次创建该闭包指定value为1,函数地址为0xc00000e028,进行两次闭包调用可以发现值依次增长为2和3
- 第二次创建该闭包指定value为10,函数地址为0xc00000e038,进行一次闭包调用可以发现值变为11
- 以上测试可见,两次生成的闭包依次绑定了自身的环境,并且一直保持绑定
生成指定后缀文件名称的例子:
func BuildFileSuffix(suffix string) func(fileName string) string {
return func(fileName string) string {
// 闭包绑定环境变量suffix
// 闭包自身输入fileName,组成完成文件名
return fmt.Sprintf("%v.%v", fileName, suffix)
}
}
func testBuildFileSuffix() {
// 后缀名java
javaFunc := BuildFileSuffix("java")
fmt.Println(javaFunc("file1")) // file1.java
fmt.Println(javaFunc("file2")) // file2.java
// 后缀名golang
golangFunc := BuildFileSuffix("golang")
fmt.Println(golangFunc("file3")) // file3.golang
fmt.Println(golangFunc("file4")) // file4.golang
}
1.3.2.延迟绑定
闭包会在使用时实际读取环境变量值,而不是取生成闭包时的环境变量值。
下面的例子里在闭包f生成时x的值为1,然后将x值设置为2,此时返回闭包。闭包在使用时x值已经变为2,所以打印的值为2。
func DelayBidding() func() {
x := 1
f := func() {
fmt.Println(x)
}
x = 2
return f
}
func testDelayBidding() {
DelayBidding()() // 2
}
2.for range延迟绑定问题
在Golang中使用for range时,会存在闭包的延迟绑定问题,该问题是Golang经典问题,在开发时也该格外注意。
2.1.for range复用
for range的惯用法是使用短变量声明方式(:=)在for的initStmt中声明迭代变量(iteration variable)。但需要注意的是,这些迭代变量在for range的每次循环中都会被重用,而不是重新声明:
func forRange() {
list := []int{1, 2, 3, 4, 5}
for i, v := range list {
fmt.Printf("%v-%v\n", &i, &v)
}
}
输出结果为:
0xc000094008-0xc000094010
0xc000094008-0xc000094010
0xc000094008-0xc000094010
0xc000094008-0xc000094010
0xc000094008-0xc000094010
可见,在使用for range时,生成的i和v都复用的是同一变量,所以for range表达式又可等价为:
func forRange() {
list := []int{1, 2, 3, 4, 5}
var i, v int
for i, v = range list {
fmt.Printf("%v-%v\n", &i, &v)
}
}
2.2.for range延迟绑定
所以在for range中使用闭包时,会存在延迟绑定问题,因为i和v本质上是在被复用的,所以闭包被调用时会获取到i和v的最新值:
func TestForRangeDelayBidding() {
list := []int{1, 2, 3, 4, 5}
var funcList []func()
for _, v := range list {
f := func() {
fmt.Println(v)
}
funcList = append(funcList, f)
}
for _, f := range funcList {
f()
}
}
以上结果预期为1,2,3,4,5,实际输出值为5,5,5,5,5。
再看一个使用go runtine的闭包的例子:
func TestForRangeGoRuntineDelayBidding() {
list := []int{1, 2, 3, 4, 5}
var wg sync.WaitGroup
for _, v := range list {
wg.Add(1)
go func() {
defer wg.Done()
fmt.Println(v)
}()
}
wg.Wait()
}
以上结果预期为1,2,3,4,5,实际输出值为5,5,5,5,5。
2.3.如何避免延迟绑定
for range本质上是复用了i和v,所以避免for range闭包延迟绑定的方式就是不让其复用i和v:
- 第一种方式,在闭包声明前,再创建一个新值给闭包引用,不复用原有值
func TestForRange1() {
list := []int{1, 2, 3, 4, 5}
var wg sync.WaitGroup
for _, v := range list {
wg.Add(1)
// 创建一个新值v2,v2不会被复用
v2 := v
go func() {
defer wg.Done()
// 闭包引用v2
fmt.Println(v2)
}()
}
wg.Wait()
}
- 第二种方式,闭包声明时在指定传入参数,此时发生值拷贝,不复用原有值
func TestForRange2() {
list := []int{1, 2, 3, 4, 5}
var wg sync.WaitGroup
for _, v := range list {
wg.Add(1)
// 将值拷贝传入闭包
go func(v2 int) {
defer wg.Done()
fmt.Println(v2)
}(v)
}
wg.Wait()
}