go语言闭包问题

1.什么是闭包

官方的讲,闭包是指能够包含自由变量(未绑定到特定对象)变量的代码块;这些变量不是在这个代码块内活在任何全局上下文中定义的,而是在定义代码块的环境中定义的(局部变量), 当在这个代码块所在环境的外部调用该代码块时,代码块和它所引用的自由变量构成闭包。以下图所示。
图片.png

2.闭包的做用

从图1很容易看出,自由变量是局部变量,外部没法访问,但和它同属一个局部环境的代码块却能够访问,所以,咱们能够经过代码块间接地访问内部的自由变量。若是代码块理解起来有点儿抽象,咱们下面看一个具体的例子:
package main
    import "fmt"

    func A() func(){
        // n是自由变量
        var n int = 2019
        // 匿名函数至关于图1中的“代码块”
        return func() {
            n += 1
            fmt.Println(n)
        }
    }
    // main()在外部环境中
    func main() {
        myFunc := A()
        // 外部环境调用代码块
        myFunc()
        myFunc()
    }
在上面的代码中,函数A内部有一个局部变量n, 为了能在外部环境中访问n,咱们让函数A返回了一个匿名函数,这个匿名函数有对n的访问权限,咱们在main函数调用函数A返回的匿名函数,能够修改并打印出n的值,代码编译运行的结果以下:
2020
    2021

    Process finished with exit code 0
能够看出,这个A内部的匿名函数就是函数A暴露给外部访问其内部变量n的一个“接口”,经过这个接口,调用者能够实如今全局环境中访问局部变量。

3.闭包的好处

  • 咱们再来观察上面代码的运行结果,发如今main函数中两次调用A的匿名函数,结果竟然不一样。
  • 通常状况下,在函数func执行完后,函数以前申请的栈空间会被释放,函数中的局部变量也会被销毁,下次再出现函数调用时,从新申请栈空间,而且从新初始化函数内部的局部变量。
  • 可是在闭包调用的状况下,状况会变得不一样,因为在main函数调用了A返回的匿名函数,至关于myFunc = func() {n += 1; fmt.Println(n);},而且匿名函数内部引用着A里的局部变量n,因此致使在main函数一次函数调用结束后,n没法被销毁,由于它此时整被main函数中的myFunc引用着,它的生命周期和myFunc的生命周期相同,在main函数执行完毕时才会被释放。
  • 所以,当使用闭包方式访问某个局部变量时,该局部变量会常驻内存,访问速度会特别快,而且咱们始终操做的都是这个变量的自己,而不是其副本,修改时也特别方便。闭包调用的大体过程以下图所示(不是很准确,会意便可!)
图片.png
  • 为了更好地理解闭包,咱们再看一个例子。编程

    • 编写一个函数makeSuffix(suffix string)能够接收一个文件后缀名(好比.jpg),并返回一个匿名函数;
    • 调用闭包,能够传入一个文件名,若是该文件名没有指定的后缀(好比.jpg),则返回文件名.jpg, 若是已经有.jpg后缀,则返回原文件名。
    • 代码以下所示:
package main

        import (
            "fmt"
            "strings"
        )

        // 处理文件名
        func makeSuffix(suffix string) func (string) string {
            return func(name string) string {
                // 匿名函数绑定的局部变量是外部函数形参suffix
                if !strings.HasSuffix(name, suffix) {
                    // 若是文件名没有指定的后缀名,则给它加上指定的后缀名
                    name += suffix
                }
                return name
            }
        }

        func main() {
            // 1.指定文件后缀名为.jpg
            f := makeSuffix(".jpg")
            // 2.建立文件名
            fileName1 := "flower"
            fileName2 := "flower.jpg"
            // 3.调用f
            res1 := f(fileName1)
            res2 := f(fileName2)
            // 4.打印闭包调用处理后的结果
            fmt.Println("res1=", res1)
            fmt.Println("res2=", res2)
        }
  • 编译运行后的结果以下:
res1= flower.jpg
        res2= flower.jpg

        Process finished with exit code 0
  • 从结果能够看出,原来不是之后缀.jpg结尾的文件名被加上了.jpg,以.jpg结尾的文件名没有变化。
  • 在上述代码中,返回的匿名函数和makeSuffix(suffix string)的suffix变量组成了闭包关系,由于返回的匿名函数引用到了这个变量。
  • 咱们体会一下闭包的好处,若是使用传统的方法,也能够很容易实现这个功能,可是传统须要每次都传入后缀名,好比.jpg, 而闭包是由于初次调用时闭包绑定的变量已常常驻内存,因此传入一次就能够反复使用。

4.闭包的总结

(1) 一般是经过嵌套的匿名函数的形式实现的;
(2) 匿名函数内部引用了外部函数的参数或变量;
(3) 被引用的参数和变量的生命周期和外部调用者的生命周期相同。

5.防止闭包的误用

请看下面的代码:
package main

    import (
        "fmt"
        "sync"
    )

    var wg sync.WaitGroup
    func main() {
        wg.Add(10);
        for i:=0; i<10; i++ {
            // 以匿名函数的形式开启goroutine
            go func() {
                fmt.Println(i)
                wg.Done()
            }()
        }
        wg.Wait()
    }
  • 在main函数中因为在匿名函数引用了外部变量i,所以匿名函数以闭包的形式访问i, 但同时for循环中使用了goroutine,因为goroutine之间是并发执行的,再参考图2闭包调用的流程图,就会出现多个goroutine访问同一个内存中的变量,会出现“脏读”现象,代码编译执行以下:
2
        7
        7
        3
        7
        10
        10
        10
        10
        7

        Process finished with exit code 0
  • 为了解决这个问题,也很简单,咱们只需让for循环时每一个匿名函数绑定的不是外部变量i,而是i的副本,如何解决呢?之间用匿名函数传参的形式,因为go语言的函数传参都是值传递,这样就能够经过值传递来为每一个goroutine的匿名函数复制出一个当前i的副本,全部的goroutine在同时执行时互不影响。代码以下:
package main

        import (
            "fmt"
            "sync"
        )

        var wg sync.WaitGroup
        func main() {
            wg.Add(10);
            for i:=0; i<10; i++ {
                // 以匿名函数的形式开启goroutine
                go func(num int) {
                    fmt.Println(num)
                    wg.Done()
                }(i)
            }
            wg.Wait()
        }
编译执行结果以下:
5
        6
        2
        0
        7
        9
        3
        8
        1
        4

        Process finished with exit code 0
能够发现,每一个goroutine打印的结果都不同,打印顺序随机是因为goroutine之间的并发执行形成的,咱们经过匿名函数传参的形式就解决了这种因为不经意间使用了闭包(本身没有发现)带来的错误,在go语言的并发编程中,这种状况比较多见,咱们应该格外注意。

闭包问题的讨论就到这里了,各位看官下期再见~~~

我是lioney,年轻的后端攻城狮一枚,爱钻研,爱技术,爱分享。
我的笔记,整理不易,感谢阅读、点赞和收藏。
文章有任何问题欢迎你们指出,也欢迎你们一块儿交流后端各类问题!
相关文章
相关标签/搜索