golang的闭包函数示例

golang的高级语法中,有个闭包函数的概念,仔细研究了一下,还挺有意思的,这里举几个例子说明一下

什么是闭包

闭包可以理解成“定义在一个函数内部的函数”。在本质上,闭包是将函数内部和函数外部连接起来的桥梁。或者说是函数和其他引用环境的组合体。

基础示例和defer延迟注册

//函数片段
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)) //执行顺序:t1(1) t1(2) println
	t2 := add(100)
	fmt.Println(t2(1), t2(2))
}

结果:

0xc04204a080
0xc04204a080
0xc04204a080
11 13
0xc04204a0b8
0xc04204a0b8
0xc04204a0b8
101 103

根据程序的执行结果可以看出来,内部函数是对外部变量引用。

延迟调用有些知识点有异曲同工的地方,函数体内某个变量作为defer匿名函数的参数,则在定义defer时已获得值拷贝,否则引用某个变量的地址(引用拷贝)。代码片段如下所示:

//由 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)
}

结果:

101 202
defer x, y = 1 202
//由 main 函数作为程序入口点启动
func main() {
	for i := 0; i < 3; i++ {
	//多次注册延迟调用,相反顺序执行
		defer func(){
			fmt.Println(i) //闭包引用局部变量
		}()
		fmt.Print(i)
		if i == 2 {
			fmt.Printf("\n")
		}
	}
}

结果:

012
3
3
3

返回多个内部函数

//返回加减函数,重点:内部函数时对外部变量的引用

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))
}

结果:

0xc042010098
0xc042010098
0xc042010098
101 99
0xc042010098
0xc042010098
102 98
0xc042010098
0xc042010098
103 97

涉及 goroutine 时的情况

//由 main 函数作为程序入口点启动
func main() {
	for i:=0; i<5; i++ {
		go func(){
			fmt.Println(i) //i变量值也是引用.创建5个线程执行函数, for循环执行过程中可能执行完的时候,线程刚好处于i的某个值。 
		}()
	
	}
	time.Sleep(time.Second * 1)
}

结果:

5
5
5
5
5

代码改进:

//由 main 函数作为程序入口点启动
func main() {
	ch := make(chan int, 1)

	for i:=0; i<5; i++ {
		go func(){
			fmt.Println(i)
			ch <- 1
		}()

		<- ch
	}
	time.Sleep(time.Second * 1)
}

结果:

0
1
2
3
4

闭包错误引起的死锁

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个线程执行函数,并向channel写入值。由于goroutine还没开始,i的值已经跑到了最大的9或者其他,使得这几个goroutine都取的i=9这个值,从而都向cs[9]发消息,导致执行t := <-cs[i]时,cs[0]、cs[1]、cs[2] … 都阻塞起来了,从而导致了死锁。

代码修改如下:两种方法修改

for i := range cs {
        go func(index int) {
            cs[index] <- index
        }(i) //值传递,已经传进去了,而不是共用引用环境的变量
}

第二种的原理就是保证每次循环都是协同写入之后才开始遍历下一次。

ch := make(chan int)
for i := range cs {
	go func() {
		cs[i] <- i
		ch <- 1
	}()
	<- ch
}

综合案例

func main(){
	var fs = [4]func(){}

	for i := 0; i < 4; i++ {
		defer fmt.Println("defer i = ", i) // 不是闭包函数,所以直接是值拷贝不是引用
		defer func() { fmt.Println("defer _closure i = ", i) }() //()必须有,否则表示函数地址
		fs[i] = func() {  fmt.Println("closure i = ", i) }
	}

	for _, f := range fs {
		f()
	}
}

结果:

closure i = 4
closure i = 4
closure i = 4
closure i = 4
defer _closure i = 4
defer i = 3
defer _closure i = 4
defer i = 2
defer _closure i = 4
defer i = 1
defer _closure i = 4
defer i = 0

标题:golang的闭包函数示例
作者:reyren
地址:https://www.reyren.cn/articles/2021/07/05/1625480683980.html

    0 评论
avatar