Go-01-闭包

闭包概念

所谓闭包是指内层函数引用了外层函数中的变量或称为引用了自由变量的函数,其返回值也是一个函数

1
2
3
闭包 = 函数 + 引用环境

接受一个或多个函数作为输入 输出一个函数

函数

函数只是一段可执行代码,编译后就“固化”了,每个函数在内存中只有一份实例,得到函数的入口点便可以执行函数了。

在函数式编程语言中,函数是一等公民(First class value):第一类对象,我们不需要像命令式语言中那样借助函数指针,委托操作函数,函数可以作为另一个函数的参数或返回值,可以赋给一个变量。函数可以嵌套定义,即在一个函数内部可以定义另一个函数,有了嵌套函数这种结构,便会产生闭包问题

匿名函数

函数可以像普通的类型(整型、字符串等)一样进行赋值、作为函数的参数传递、作为函数的返回值等。

匿名函数可以动态的创建,与之成对比的常规函数必须在包中编译前就定义完毕。匿名函数可以随时改变功能

Golang的函数只能返回匿名函数!

闭包实例

实例1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//函数片段
func add(base int) func(int) int {

fmt.Printf("%p\n", &base) //打印变量地址,可以看出来 内部函数时对外部传入参数的引用

f := func(i int) int {
fmt.Printf("%p\n", &base)
base += i
return base
}

return f
}

//由 main 函数作为程序入口点启动
func main() {
t1 := add(10)
fmt.Println(t1(1), t1(2))

t2 := add(100)
fmt.Println(t2(1), t2(2))

}

运行结果为

1
2
3
4
5
6
7
8
9
0xc0000b4008
0xc0000b4008
0xc0000b4008
11 13
0xc0000b4020
0xc0000b4020

0xc0000b4020
101 103

可以看出:

  1. 函数add返回一个函数,返回的这个函数就是闭包

  2. 调用同一个闭包函数,都是对同一个环境进行操作

  3. 函数add每进入一次,就形成了一个新的环境,对应的闭包中,函数都是同一个函数,环境却是引用不同的环境

实例2.1:

1
2
3
4
5
6
7
8
9
10
11
12
13
//由 main 函数作为程序入口点启动
func main() {
x, y := 1,2

defer func(a int){
fmt.Println("defer x, y = ", a, y) //y为闭包引用
}(x) //x值拷贝 调用时传入参数

x += 100
y += 200

fmt.Println(x, y)
}

结果为

1
2
101 202
defer x, y = 1 202

闭包对外的变量是引用

实例2.2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//由 main 函数作为程序入口点启动
func main() {
for i := 0; i < 3; i++ {
//多次注册延迟调用,相反顺序执行
defer func(){
fmt.Println(i) //闭包引用局部变量
}()

fmt.Print(i)
if i == 2 {
fmt.Printf("\n")
}
}
}

结果为

1
2
3
4
012
3
3
3

实例3:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
//返回加减函数,重点:内部函数时对外部变量的引用
func calc(base int) (func(int) int, func(int) int) {

fmt.Printf("%p\n", &base)
add := func(i int) int {
fmt.Printf("%p\n", &base)
base += i
return base
}

sub := func(i int) int {
fmt.Printf("%p\n", &base)
base -= i
return base
}

return add, sub
}

//由 main 函数作为程序入口点启动
func main() {
f1, f2 := calc(100)

fmt.Println(f1(1), f2(2)) //执行顺序:f1 f2 println
fmt.Println(f1(3), f2(4))
fmt.Println(f1(5), f2(6))
fmt.Println(f1(7), f2(8))
}

结果为

1
2
3
4
5
6
7
8
9
10
11
12
13
0xc00001a088
0xc00001a088
0xc00001a088
101 99
0xc00001a088
0xc00001a088
102 98
0xc00001a088
0xc00001a088
103 97
0xc00001a088
0xc00001a088
104 96

可以看出:

  1. 可利用go特性,同时返回两个闭包函数

    1. 相同父环境得两个闭包函数,使用的是相同的内存变量

实例4:

1
2
3
4
5
6
7
8
9
10
11
//由 main 函数作为程序入口点启动
func main() {

for i:=0; i<5; i++ {
go func(){
fmt.Println(i) //i变量值也是引用.创建5个线程执行函数, for循环执行过程中可能执行完的时候,线程刚好处于i的某个值。
}()

}
time.Sleep(time.Second * 1)
}

结果为

1
2
3
4
5
5
5
5
5
5

闭包中的值是对源变量的引用。指向的是变量的当前值。当延迟调用函数体内某个变量作为defer匿名函数的参数,则在定义defer时已获得值拷贝,否则引用某个变量的地址(引用拷贝)

实例5(阻塞):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func main() {
//创建slice
cs := make([](chan int), 10)
for i := 0; i < len(cs); i++ {
cs[i] = make(chan int)
}

for i := range cs {
go func() {
cs[i] <- i //创建线程,但是i是引用外部变量,不一定等线程执行的时候就是当前i值
}()
}

for i := 0; i < len(cs); i++ {
t := <-cs[i] //读取值的时候,可能会出现一只阻塞的情况
fmt.Println(t)
}
}

主要功能就是 创建10个线程执行函数,并向channal写入值。由于goroutine还没有开始,i的值已经跑到了最大9,使得这几个goroutine都取的i=9这个值,从而都向cs[9]发消息,导致执行t := <-cs[i]时,cs[0]、cs[1]、cs[2] … 都阻塞起来了,从而导致了死锁

方案1(利用闭包传每次循环得参数,利用参数进行值传递):

1
2
3
4
5
for i := range cs {
go func(index int) {
cs[index] <- index
}(i)
}

方案2(利用阻塞管道,每次等待协程起来再执行下一次):

1
2
3
4
5
6
7
8
ch := make(chan int)
for i := range cs {
go func() {
ch <- 1
cs[i] <- i
}()
<- ch
}

参考链接

  1. https://tiancaiamao.gitbooks.io/go-internals/content/zh/03.6.html