Golang闭包

定义

函数可以嵌套定义(嵌套的函数一般为匿名函数),即在一个函数内部可以定义另一个函数。Go语言通过匿名函数支持闭包,C++不支持匿名函数,在C++11中通过Lambda表达式支持闭包。
闭包是由函数及其相关引用环境组合而成的实体(即:闭包=函数+引用环境)。

引用环境的定义:

在函数式语言中,当内嵌函数体内引用到体外的变量时,将会把定义时涉及到的引用环境和函数体打包成一个整体(闭包)返回。当每次调用包含闭包的函数时,都将返回一个新的闭包实例,这些实例之间是隔离的,分别包含调用时不同的引用环境现场。

不同于函数,闭包在运行时可以有多个实例,不同的引用环境和相同的函数组合可以产生不同的实例

关键知识点

  1. 闭包与逃逸分析

    闭包可能会导致变量逃逸到堆上来延长变量的生命周期,给GC带来压力

    func closure() func(int) int{
    	var x int
    	return func(a int) int {
    		x++
    		return a + x
    	}
    }
    func main() {
    	a := closure() //x逃逸到堆上,成为a闭包实例的一部分
    	fmt.Println(a(1)) //2
    	fmt.Println(a(1)) //3
    }
    
    
  2. 闭包与外部函数的生命周期

    注意:当closure函数被重新调用后,返回的是新的闭包实例,引用的变量也是重新在堆上定义的。

    func closure1(n int) func() {
    	n++
    	return func()  {
    		fmt.Println(n)
    	}
    }
    
    func closure2(n int) func() {
      return func(){
        n++
        fmt.Println(n)
      }
    }
    
    func main() {
    	a := closure1(3) //变量被匿名函数引用,closure函数结束,n变量也不会马上销毁,而是绑定到了匿名函数上
      b := closure2(3) //
    	a() //4
    	a() //4
      b() //4
      b() //5
    }
    
  3. 闭包在for循环中问题

    func main() {
    	s := []string{"a", "b", "c"}
    	for _, v := range s {
    		go func() {
    			fmt.Println(v)
    		}()
    	}
    	time.Sleep(time.Second * 1)
    }
    
    // c c c
    

    主协程执行完for之后,定义的子协程才开始执行,v最终是c,所以输出了c c c。如果for过程中,子协程执行了,结果就可能不是c, c,c。输出的结果依赖于子协程执行时的那一刻,v是什么

     func main() {        
          s := []string{"a", "b", "c"}
        for _, v := range s {
            go func() {
                fmt.Println(v)
            }()
            time.Sleep(time.Second * 3)
        }
        fmt.Println("main routine")
        time.Sleep(time.Second * 1)    // 阻塞模式
    }
    
    //a b c
    
    //还有一种办法就是需要每次将变量v拷贝给函数即可,但此时就不是使用上下文环境中的变量了
    func main() {                
        s := []string{"a", "b", "c"}                             
        for _, v := range s { 
            go func(c string) {
                fmt.Println(c)
            }(v)   //每次将变量 v 的拷贝传进函数                 
        }                        
        select {}                                                      
    }  
    
  4. 延迟调用与闭包

    defer中使用匿名函数依然是一个闭包

    package main
    
    import "fmt"
    
    func main() {
        x, y := 1, 2
    
        defer func(a int) { 
            fmt.Printf("x:%d,y:%d\n", a, y)  // y 为闭包引用
        }(x)      // 复制 x 的值
    
        x += 100
        y += 100
        fmt.Println(x, y)
    }
    
    /*
    101 102
    x:1 y:102
    */
    

    在defer定义时候已经将x的值1拷贝进了defer函数,defer执行时使用的是defer定义时x的拷贝,而不是当前环境中x的值

总结

其实闭包需要注意的关键点就是:当闭包引用外部变量时候,此变量的生命周期就不是它的作用域范围了,而是被闭包实例捕获,此时闭包不仅仅和外部函数共享此变量,更重要的是变量的生命周期因匿名函数也就是闭包的存在而延长

posted @ 2021-08-08 16:03  尹瑞星  阅读(558)  评论(0编辑  收藏  举报