go 闭包与匿名函数这一篇就够了

最近在看go语言中的defer,里面涉及到了闭包,之前只是对闭包有了解,但是并没有深入的研究过,这次就深入研究一下吧。本文主要从如下几个点来展开描述,希望对你有所帮助~

  • 1、匿名函数的定义与实现
    2、闭包的定义 [穿插讲引用环境的定义]
    3、闭包的实现
    4、关于闭包你需要掌握的几个点:
    (1)闭包与逃逸分析
    (2)闭包与外部函数的生命周期
    (3)通过for循环的案例分析闭包对引用环境中变量的调用问题
    (4)当closure所在函数重新调用时,其closure是新的,其context引用的变量也是重新在heap定义过的。
    5、延迟调用与闭包

1 匿名函数的定义与实现

      顾名思义,函数没有名字,跟正常的函数调用区别不大。 Go 里面匿名函数与正常的函数区别,参数的传递区别不大,只是在调用方面,匿名函数需要通过一个包装对象`func1.f`` 来调用匿名函数,这个过程通过 rbx 进行二次寻址来完成调用。理论上,匿名函数也会比正常函数性能要差。 具体可以参考closure in go

2 闭包的定义

闭包的定义

闭包是由函数及其相关的引用环境组合而成的实体(即:闭包=函数+引用环境)。

引用环境的定义
  • 在函数式语言中,当内嵌函数体内引用到体外的变量时,将会把定义时涉及到的引用环境和函数体打包成一个整体(闭包)返回。现在给出引用环境的定义就容易理解了:引用环境是指在程序执行中的某个点所有处于活跃状态的约束(一个变量的名字和其所代表的对象之间的联系)所组成的集合。闭包的使用和正常的函数调用没有区别。
  • 由于闭包把函数和运行时的引用环境打包成为一个新的整体,所以就解决了函数编程中的嵌套所引发的问题。当每次调用包含闭包的函数时都将返回一个新的闭包实例,这些实例之间是隔离的,分别包含调用时不同的引用环境现场。不同于函数,闭包在运行时可以有多个实例,不同的引用环境和相同的函数组合可以产生不同的实例。

3 闭包的实现

       闭包的实现也是一个匿名函数,一级指针在匿名函数里面指向“func1.f”, 在闭包中,指向闭包返回对象。闭包返回的包装对象是一个复合结构,里面包含匿名函数的地址,以及环境变量的地址。

4 关于闭包你需要掌握的几个点

(1)闭包与逃逸分析

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


testClosure.png

可以执行一下如下指令:

go build -gcflags "-N -l -m" closure  

看下结果


image.png

(2)闭包与外部函数的生命周期

      内函数对外函数的变量的修改,是对变量的引用。共享一个在堆上的变量。 变量被引用后,它所在的函数结束,这变量也不会马上被销毁。相当于变相延长了函数的生命周期。
看个例子:

func AntherExFunc(n int) func() {
    n++
    return func() {
        fmt.Println(n)
    }
}

func ExFunc(n int) func() {
    return func() {
        n++
        fmt.Println(n)
    }
}

func main() {
    myAnotherFunc:=AntherExFunc(20)
    fmt.Println(myAnotherFunc)  //0x48e3d0  在这儿已经定义了n=20 ,然后执行++ 操作,所以是21 。
    myAnotherFunc()     //21 后面对闭包的调用,没有对n执行加一操作,所以一直是21
    myAnotherFunc()     //21

    myFunc:=ExFunc(10)
    fmt.Println(myFunc)  //0x48e340   这儿定义了n 为10
    myFunc()       //11  后面对闭包的调用,每次都对n进行加1操作。
    myFunc()       //12

}

(3)通过for循环的案例分析闭包对引用环境中变量的调用问题

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

结果会是什么呢? a, b, c? 错了,结果是 c, c, c。为什么呢?
这是因为for语句里面中闭包使用的v是外部的v变量,当执行完循环之后,v最终是c,所以输出了 c, c, c。 如果你去执行,有可能也不是这个结果。 输出这个结果的前提是“在主协程执行完for之后,定义的子协程 才开始执行,如果for过程中,子协程执行了,结果就可能不是c, c,c”。 输出的结果依赖于子协程执行时的那一刻,v是什么。

之前 这个地方留了个疑问,是否是主协程中有闭包时,应该会先把闭包定义好,先不执行,当主协程执行完之后,再执行闭包所在的协程,此时协程使用环境变量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 , main routine
为什么这次有正常了呢? 这是因为在for循环中执行了sleep, 让每次for循环中新定义的子协程有时间执行,子协程执行时获取环境中的变量v, 那么每次就会是本次循环执行时变量v的实际值。

如果想输出b, b, c 呢?可以看下面的代码:

func main() {
  s := []string{"a", "b", "c"}
    for k, v := range s {
        go func() {
            fmt.Println(v)
        }()
        // 在执行到第二次时,sleep一会,此时已经定义的两个子协程得到执行,而此时的v为b
        if k == 1 {
            time.Sleep(time.Second * 3)
        }
    }
    fmt.Println("main routine")
    time.Sleep(time.Second * 1)    // 阻塞模式
}

最开始的for程序如果想输出a,b,c还有一种解决方法:
只需要每次将变量v的拷贝传进函数即可,但此时就不是使用的上下文环境中的变量了。

func main() {                
    s := []string{"a", "b", "c"}                             
    for _, v := range s { 
        go func(v string) {
            fmt.Println(v)
        }(v)   //每次将变量 v 的拷贝传进函数                 
    }                        
    select {}                                                      
}  

(4)当closure所在函数重新调用时,其closure是新的,其context引用的变量也是重新在heap定义过的。

func main() {
    nextInt := intSeq()
    println(unsafe.Pointer(&nextInt))
    println(nextInt()) // 1
    println(nextInt()) // 2
    println(nextInt()) // 3

    newInts := intSeq()
    println(unsafe.Pointer(&newInts))
    println(newInts()) // 1

}

func intSeq() func() int {
    i := 0
    println("closure init--------------")
    println(unsafe.Pointer(&i))
    return func() int {
        i += 1
        println("in closure")
        println(unsafe.Pointer(&i))
        return i
    }
}

看下结果:

closure init--------------
0xc00001a068
0xc000042778
in closure
0xc00001a068
1
in closure
0xc00001a068
2
in closure
0xc00001a068
3
closure init--------------
0xc00001a080
0xc000042780
in closure
0xc00001a080
1

5 延迟调用与闭包

defer 调用会在当前函数执行结束前才被执行,这些调用被称为延迟调用 。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而不是101呢?其实是原因是 在defer定义时 已经将x的拷贝 1 复制给了defer, defer执行时使用的是当时defer定义时x的拷贝,而不是当前环境中x的值。

6 总结

本文分析了匿名函数、闭包的定义与实现,然后通过实例分析了闭包的一些重要指示以及易错点,希望对你有用~

参考文献

http://sunisdown.me/closures-in-go.html
https://www.jianshu.com/p/fa21e6fada70
http://alexstocks.github.io/html/go-closure.html
https://studygolang.com/articles/11239
https://gocn.vip/article/1607

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269