0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

什么是闭包的延迟绑定

马哥Linux运维 来源:马哥Linux运维 作者:马哥Linux运维 2022-09-02 10:39 次阅读

本文讲介绍以下几个内容:

引入用GoLang语言写的几个case;

介绍什么是闭包;

介绍什么是闭包的延迟绑定;

从闭包的延迟绑定讲到GoLang的Go Routine的延迟绑定问题;

I. 几个有趣的Case

开门见山,首先请各位看官们先看下面foo1()到foo7()一共7个函数,然后回答后面的问题。(一下子丢出7个函数,请见谅。不过,每个函数都非常简短,而本文接下来将围绕这7个函数展开,因此,请各位看官老爷们耐心且看看题,活动活动脑细胞~)

case 1:


func foo1(x *int) func() {  return func() {    *x = *x + 1    fmt.Printf("foo1 val = %d
", *x)  }}
case 2:

func foo2(x int) func() {  return func() {    x = x + 1    fmt.Printf("foo2 val = %d
", x)  }}
case 3:

func foo3() {  values := []int{1, 2, 3, 5}  for _, val := range values {    fmt.Printf("foo3 val = %d
", val)  }}
case 4:

func show(v interface{}) {  fmt.Printf("foo4 val = %v
", v)}func foo4() {  values := []int{1, 2, 3, 5}  for _, val := range values {    go show(val)  }}
case 5:

func foo5() {  values := []int{1, 2, 3, 5}  for _, val := range values {    go func() {      fmt.Printf("foo5 val = %v
", val)    }()  }}
case 6:

var foo6Chan = make(chan int, 10)func foo6() {  for val := range foo6Chan {    go func() {      fmt.Printf("foo6 val = %d
", val)    }()  }}
case 7:

func foo7(x int) []func() {  var fs []func()  values := []int{1, 2, 3, 5}  for _, val := range values {    fs = append(fs, func() {      fmt.Printf("foo7 val = %d
", x+val)    })  }  return fs}

Q1:

第一组实验:假设现在有变量x=133,并创建变量f1和f2分别为foo1(&x)和foo2(x)的返回值,请问多次调用f1()和f2()会打印什么?

第二组实验:重新赋值变量x=233,请问此时多次调用f1()和f2()会打印什么?

第三组实验:如果直接调用foo1(&x)()和foo2(x)()多次,请问每次都会打印什么?

Q2:
请问分别调用函数foo3(),foo4()和foo5(),分别会打印什么?

Q3:

第一组实验:如果“几乎同时”往channelfoo6Chan里面塞入一组数据"1,2,3,5",foo6会打印什么?

第二组实验:如果以间隔纳秒(10^-9秒)的时间往channel里面塞入一组数据,此时foo6又会打印什么?

第三组实验:如果是微秒(10^-6秒)呢?如果是毫秒(10^-3秒)呢?如果是秒呢?

Q4:
请问如果创建变量f7s=foo7(11),f7s是一个函数集合,遍历f7s会打印什么?

接下来,我们逐一来看这些问题和对应的foo函数。

II. case1~2:值传递(by value) vs. 引用传递(by reference)

子标题好难起 >0<...   看到case1和case2的两组函数foo1()和foo2(),相信各位看官就知道,其中一个知识点就是值传递和引用传递。   其实呢,Go是没有引用传递的,即使是foo1()在参数上加了*,内部实现机制仍旧是值传递,只不过传递的是指针的数值。但是为了称呼方便,下文会成为“引用传递”(为了区分正确的引用传递,这里特意加了引号)。   如下图所示,我们的目的是传递X变量,于是我们创建了一个传参地址(临时地址变量),它存放了X变量的地址值,调用函数的时候给它这个传参地址,函数呢,则会再创建一个入参地址,是传参地址的一份拷贝。函数拿到了这个地址值,可以通过寻址拿到这个X变量,此时函数如果直接修改X变量可以认为是“本地修改”或者“永久修改”了这个变量的数值。   「举个生活中的例子,比如一个叫做“函数”的人想寻找一个叫做“X”的人,函数跑过来问知道X的我,我拿出地址簿,给他出示了X这个人的家庭地址,函数记性不太好,所以拿了一本本子把X的地址抄在了他自己的本子上。」   这个例子中,我的那个记着X的家庭地址的地址簿,就是传参地址;函数抄录了X地址的本子,就是入参地址;X的家庭地址,就对应了X变量的地址值。(哎,为什么讲的这么细节了?) 2e5dc4ec-2a04-11ed-ba43-dac502259ad0.jpg   Golang的“引用传递”   话题似乎有点扯远了,拉回来,我们再来看看foo1()和foo2()。   foo1()和foo2()的区别确实在于值传递和引用传递,但是这个并不是本文介绍的中心。本文要介绍的已经在标题上写明了:闭包(closure)。  

闭包(closure)

什么是闭包呢?摘用Wikipedia上的一句定义:

aclosureis a record storinga functiontogether withan environment.
闭包是由函数和与其相关的引用环境组合而成的实体 。

因此闭包的核心就是:函数和环境。其实这里就已经可以回答本文题目的问题:闭包究竟包了什么?答案是:函数和环境。但是相信部分看官们到这里依然不清楚:什么函数?什么环境? 函数,指的是在闭包实际实现的时候,往往通过调用一个外部函数返回其内部函数来实现的。内部函数可能是内部实名函数、匿名函数或者一段lambda表达式。用户得到一个闭包,也等同于得到了这个内部函数,每次执行这个闭包就等同于执行内部函数。 环境,Wikipedia上说是与其(函数)相关的引用环境,可以说解释地很精准了。 具体地说,在实际中引用环境是指外部函数的环境,闭包保存/记录了它产生时的外部函数的所有环境。但是这段话对于尚未理解闭包的同学来说依旧是不友好的,听完还是懵懂的。这里尝试做个更实用性的解释:

如果外部函数的所有变量可见性都是local的,即生命周期在外部函数结束时也结束的,那么闭包的环境也是封闭的。

反之,那么闭包其实不再封闭,全局可见的变量的修改,也会对闭包内的这个变量造成影响。

跳回foo1()和foo2()的例子,正好来解释闭包的函数和环境。


func foo1(x *int) func() {    return func() {        *x = *x + 1        fmt.Printf("foo1 val = %d
", *x)    }}func foo2(x int) func() {    return func() {        x = x + 1        fmt.Printf("foo1 val = %d
", x)    }}
// Q1第一组实验x := 133f1 := foo1(&x)f2 := foo2(x)f1() f2()f1()f2()// Q1第二组x = 233f1()f2()f1()f2()// Q1第三组foo1(&x)()foo2(x)()foo1(&x)()foo2(x)()
定义了x=133之后,我们获取得到了f1=foo1(&x)和f2=foo2(x)。这里f1f2就是闭包的函数,也就是foo1()foo2()的内部匿名函数;而闭包的环境即外部函数foo1()foo2()的变量x(因为内部匿名函数引用到的相关变量只有x,因此这里简化为变量x)。 闭包的函数做的事情归纳为:1). 将环境的变量x自增1;2). 打印环境变量x。 闭包的环境则是其外部函数获取到的变量x。 因此Q1第一组实验的答案为:

f1() // foo1 val = 134f2() // foo2 val = 134f1() // foo1 val = 135f2() // foo2 val = 135
这是因为闭包f1f2都保存了x=133时的整个环境,每次调用闭包f1f2都会执行一次自增+打印的内部匿名函数。因此第一次输出都是(133+1=)134,第二次输出都是(134+1=)135。 那么Q1第二组实验的答案呢?

f1() // foo1 val = 234f2() // foo2 val = 136f1() // foo1 val = 235f2() // foo2 val = 137
有趣的事情发生了!f1的值居然发生了显著性的变化!通过这组实验,能够更好地解释其(函数)相关的引用环境其实就是产生这个闭包的时候的外部函数的环境,因此变量x的可见性和作用域也与外部函数相同,又因为foo1是“引用传递”,变量x的作用域不局限在foo1()中,因此当x发生变化的时候,闭包f1内部也变化了。这个也正好是"反之,那么闭包其实不再封闭,全局可见的变量的修改,也会对闭包内的这个变量造成影响"的证明。 Q1的第三组实验的答案:

foo1(&x)() // foo1 val = 236foo2(x)() // foo2 val = 237foo1(&x)() // foo1 val = 237foo2(x)() // foo2 val = 238foo2(x)() // foo2 val = 238
因为foo1()返回的闭包都会修改变量x的数值,因此调用foo1()()之后,变量x必然增加1。而foo2()返回的闭包仅仅修改其内部环境的变量x而对调用外部的变量x不影响,且每次调用foo2()返回的闭包是独立的,和其他调用foo2()的闭包不相关,因此最后两次的调用,打印的数值都是相同的;第一次调用和第二次调用foo2()发现打印出来的数值增加了1,是因为两次调用之间传入的x的数值分别是236和237,而不是说第二次在第一次基础上增加了1,这点需要补充说明。

III. case7:闭包的延迟绑定

hhh,是不是以为我会接着讲case3,居然先提到了case7,意不意外惊不惊喜! 废话不多说,看官们来瞅瞅下面调用f7()的时候分别会打印什么?


func foo7(x int) []func() {    var fs []func()    values := []int{1, 2, 3, 5}    for _, val := range values {        fs = append(fs, func() {            fmt.Printf("foo7 val = %d
", x+val)        })    }    return fs}// Q4实验:f7s := foo7(11)for _, f7 := range f7s {    f7()}
答案是:

foo7 val = 16foo7 val = 16foo7 val = 16foo7 val = 16
是的,你没有看错,会打印4行,且都是16!是不是很惊喜! 相信已经有很多同学在网上看到过类似的case,并且也早已知道结果了,不清楚的同学们现在也看到答案了。嗯,这就是大名鼎鼎的闭包延迟绑定问题。网上的解释其实有很多了,这里尝试用之前对于闭包的环境的定义来解释这个现象: “ 闭包是一段函数和相关的引用环境的实体。case7的问题中,函数是打印变量val的值,引用环境是变量val。仅仅是这样的话,遍历到val=1的时候,记录的不应该是val=1的环境吗? 上文在闭包解释最后,还有一句话:闭包保存/记录了它产生时的外部函数的所有环境。如同普通变量/函数的定义和实际赋值/调用或者说执行,是两个阶段。闭包也是一样,for-loop内部仅仅是声明了一个闭包,foo7()返回的也仅仅是一段闭包的函数定义,只有在外部执行了f7()时才真正执行了闭包,此时才闭包内部的变量才会进行赋值的操作。哎,如果这么说的话,岂不是应该抛出异常吗?因为val是一个比foo7()生命周期更短的变量啊? 这就是闭包的神奇之处,它会保存相关引用的环境,也就是说,val这个变量在闭包内的生命周期得到了保证。因此在执行这个闭包的时候,会去外部环境寻找最新的数值!你是不是不相信?来来来,我们马上写个临时的case执行下分分钟就明白了:

临时的case:


func foo0() func() {    x := 1    f := func() {        fmt.Printf("foo0 val = %d
", x)    }    x = 11    return f}
foo0()() // 猜猜我会输出什么?
既然我说会在执行的时候去外部环境寻找最新的数值,那x的最新数值就是11呀,果然,最后输出的就是11。 以上就是我对于闭包的延迟绑定的通俗版本解释。:)

IV. case3~6:Go Routine的延迟绑定

case3、case4和case5不是闭包,case3只是遍历了内部的slice并且打印,case4是在遍历时通过协程调用了打印函数打印,case5也是在遍历slice时调用了内部匿名函数打印。 Q2的case3问题的答案先丢出来:


func foo3() {    values := []int{1, 2, 3, 5}    for _, val := range values {        fmt.Printf("foo3 val = %d
", val)    }}
foo3()//foo3 val = 1a//foo3 val = 2//foo3 val = 3//foo3 val = 5
中规中矩,遍历输出slice的内容:1,2,3,5。 Q2的case4问题的答案再丢出来:

func show(v interface{}) {    fmt.Printf("foo4 val = %v
", v)}func foo4() {    values := []int{1, 2, 3, 5}    for _, val := range values {        go show(val)    }}
foo4()//foo3 val = 2//foo3 val = 3//foo3 val = 1//foo3 val = 5
嗯,因为Go Routine的执行顺序是随机并行的,因此执行多次foo4()输出的顺序不一行相同,但是一定打印了“1,2,3,5”各个元素。 最后是Q2的case5问题的答案:

func foo5() {    values := []int{1, 2, 3, 5}    for _, val := range values {        go func() {            fmt.Printf("foo5 val = %v
", val)        }()    }}
foo5()//foo3 val = 5//foo3 val = 5//foo3 val = 5//foo3 val = 5
居然都打印了5,惊不惊喜,意不意外?!相信看过子标题的你,一定不意外了(捂脸)。是的,接下来就要讲讲Go Routine的延迟绑定: 其实这个问题的本质同闭包的延迟绑定,或者说,这段匿名函数的对象就是闭包。在我们调用go func() { xxx }()的时候,只要没有真正开始执行这段代码,那它还只是一段函数声明。而在这段匿名函数被执行的时候,才是内部变量寻找真正赋值的时候。 在case5中,for-loop的遍历几乎是“瞬时”完成的,4个Go Routine真正被执行在其后。矛盾是不是产生了?这个时候for-loop结束了呀,val生命周期早已结束了,程序应该报错才对呀? 回忆上一章,是不是一个相同的情境?是的,这个匿名函数可不就是一个闭包吗?一切就解释通了:闭包真正被执行的时候,for-loop结束了,但是val的生命周期在闭包内部被延长了且被赋值到最新的数值5。 不知道各位看官是否好奇,既然说Go Routine执行的时候比for-loop慢,那如果我在遍历的时候增加sleep机制呢?于是设计了Q3实验:

var foo6Chan = make(chan int, 10)func foo6() {    for val := range foo6Chan {        go func() {            fmt.Printf("foo6 val = %d
", val)        }()    }}// Q3第一组实验go foo6()foo6Chan <- 1foo6Chan <- 2foo6Chan <- 3foo6Chan <- 5// Q3第二组实验foo6Chan <- 11time.Sleep(time.Duration(1) * time.Nanosecond)foo6Chan <- 12time.Sleep(time.Duration(1) * time.Nanosecond)foo6Chan <- 13time.Sleep(time.Duration(1) * time.Nanosecond)foo6Chan <- 15// Q3第三组实验// 微秒foo6Chan <- 21time.Sleep(time.Duration(1) * time.Microsecond)foo6Chan <- 22time.Sleep(time.Duration(1) * time.Microsecond)foo6Chan <- 23time.Sleep(time.Duration(1) * time.Microsecond)foo6Chan <- 25time.Sleep(time.Duration(10) * time.Second)// 毫秒foo6Chan <- 31time.Sleep(time.Duration(1) * time.Millisecond)foo6Chan <- 32time.Sleep(time.Duration(1) * time.Millisecond)foo6Chan <- 33time.Sleep(time.Duration(1) * time.Millisecond)foo6Chan <- 35time.Sleep(time.Duration(10) * time.Second)// 秒foo6Chan <- 41time.Sleep(time.Duration(1) * time.Second)foo6Chan <- 42time.Sleep(time.Duration(1) * time.Second)foo6Chan <- 43time.Sleep(time.Duration(1) * time.Second)foo6Chan <- 45time.Sleep(time.Duration(10) * time.Second)// 实验完毕,最后记得关闭channelclose(foo6Chan)
尝试执行了多次,第一组答案如下:

foo6 val = 5/3foo6 val = 5foo6 val = 5foo6 val = 5
绝大部分时候执行出来都是5。 第二组答案如下:

foo6 val = 15/13/11/12foo6 val = 15/13foo6 val = 15foo6 val = 15
绝大部分时候执行得到的都是15。 第三组答案如下:

// 微秒foo6 val = 23/21foo6 val = 23/22foo6 val = 25/23foo6 val = 25// 毫秒foo6 val = 31foo6 val = 32foo6 val = 33foo6 val = 35// 秒foo6 val = 41foo6 val = 42foo6 val = 43foo6 val = 45
毫秒和秒的两组非常确定,顺序输出。但是微妙就不一定了,有时候是顺序输出,大部分时候是随机输出如“22,22,23,25”或者“21,22,25,25”之类的。 可见,Go Routine的匿名函数从定义到执行,耗时时间在微妙上下。于是又增加了一个临时的case测试了其真正的耗时大约是多少。

又一个临时的case:


func foo8() {    for i := 1; i < 10; i++ {        curTime := time.Now().UnixNano()        go func(t1 int64) {            t2 := time.Now().UnixNano()            fmt.Printf("foo8 ts = %d us 
", t2-t1)        }(curTime)    }}
foo8()
执行下来发现耗时在5微秒~60微秒之间不等。 但是,以上的实验数据都是从我的iMac本子上得到的,该本子的CPU是i7-7700K 4.2GHz;我又放在笔记本上(CPU为i5-8250U 1.6GHz 1.8GHz)运行了下,发现居然耗时是0微秒!起初我怀疑是时间精度的问题,于是把t1和t2时间都打印出来,精度是可以达到纳秒的。抱着仍旧不信的想法,重新运行了第三组实验,每一个都是顺序输出的! 好吧,回头再说我的iMac的问题。现在只需要记住一点:Go Routine的匿名函数的延迟绑定本质就是闭包,实际生成中注意下这种写法~

写在后面

最后,闭包是个常见的玩意儿,但是实际代码中不太建议使用,一不小心写了个内存泄漏查都查不到。特别是不要为了炫技故意写个闭包,实在没有必要。

审核编辑:彭静
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 数据
    +关注

    关注

    8

    文章

    6914

    浏览量

    88851
  • 函数
    +关注

    关注

    3

    文章

    4310

    浏览量

    62454
  • 闭包
    +关注

    关注

    0

    文章

    4

    浏览量

    2048

原文标题:Golang:“闭包(closure)”到底包了什么?

文章出处:【微信号:magedu-Linux,微信公众号:马哥Linux运维】欢迎添加关注!文章转载请注明出处。

收藏 人收藏

    评论

    相关推荐

    Linux内核驱动与单个PCI设备的绑定和解绑定

    在Linux内核2.6.13-rc3以前,驱动和设备之间的绑定和解绑只能通过insmod(modprobe)和rmmod来实现,但是这种实现方法有一个弊端,就是一旦绑定或者解绑定都是针对驱动与其
    的头像 发表于 11-17 17:11 1554次阅读
    Linux内核驱动与单个PCI设备的<b class='flag-5'>绑定</b>和解<b class='flag-5'>绑定</b>

    绑定”成功后,请问怎么通过绑定的设备向被绑定的设备发送控制命令?

    。抓都正常响应了响应的respone。但是,我不知道怎么通过绑定的设备向被绑定的设备发送控制命令。比如,一个开关和灯实现了绑定,这个绑定
    发表于 05-22 00:08

    词法作用域和

    #hello,JS:14(词法作用域)
    发表于 05-20 15:35

    ECMAScript的与匿名函数你分的清吗?

    重拾ECMAScript基础——与匿名函数
    发表于 11-08 09:39

    javascript是什么_javascript有什么作用

    首先从一个经典错误谈起,页面上有若干个div, 我们想给它们绑定一个onclick方法,于是有了下面的代码很简单的功能可是却偏偏出错了,每次alert出的值都是简单的修改就好使了让我们从一些基础的知识谈起,首先了解一下内部函数。
    发表于 12-05 10:40 1782次阅读
    javascript<b class='flag-5'>闭</b><b class='flag-5'>包</b>是什么_javascript<b class='flag-5'>闭</b><b class='flag-5'>包</b>有什么作用

    javascript原理及应用

    官方对的解释是:一个拥有许多变量和绑定了这些变量的环境的表达式(通常是一个函数),因而这些变量也是该表达式的一部分。作为一个函数变量的一个引用,当函数返回时,其处于激活状态。
    发表于 12-05 10:55 1202次阅读
    javascript<b class='flag-5'>闭</b><b class='flag-5'>包</b>原理及应用

    javascript的优缺点有哪些

    是javascript的一大难点,也是它的特色。很多高级应用都要依靠来实现。要理解
    发表于 12-05 11:10 9277次阅读

    用最简单的语言解释Python的是什么?

    很蓝瘦,你应该尽量理解一下它。
    的头像 发表于 03-21 16:33 2094次阅读

    详细介绍go语言中的的实现

    什么是? 什么场景下会用 ? 本文对 go 语言中的做了详细介绍。
    的头像 发表于 10-20 16:18 1843次阅读

    带你了解go语言中的

      【 导读】什么是? 什么场景下会用 ? 本文对 go 语言中的做了详细介绍。
    的头像 发表于 11-02 15:27 2416次阅读

    无缝绑定和非绑定支持

    自动获取并同步来自任何应用程序的绑定数据。NET源代码,包括交错数组、任何实现IListSource或IList的对象,以及任何ADO。NET数据集、DataTable、DataView或DataViewManager对象。
    的头像 发表于 01-30 15:26 442次阅读

    教程 5:配对、绑定和安全

    教程 5:配对、绑定和安全
    发表于 03-15 19:39 0次下载
    教程 5:配对、<b class='flag-5'>绑定</b>和安全

    教程 5:配对、绑定和安全

    教程 5:配对、绑定和安全
    发表于 07-06 18:49 0次下载
    教程 5:配对、<b class='flag-5'>绑定</b>和安全

    Rust语言中的应用场景

    Rust语言的是一种可以捕获外部变量并在需要时执行的匿名函数。包在Rust中是一等公民,它们可以像其他变量一样传递、存储和使用。
    的头像 发表于 09-20 11:25 571次阅读

    工业交换机的零延迟和零丢

    在现代工业自动化和网络通信的快速发展中,工业交换机作为连接各类设备的核心元素,其性能和稳定性显得尤为重要。零延迟和零丢的概念不仅是技术上的追求,更是推动工业智能化进程的重要保障。传统网络在数
    的头像 发表于 09-24 15:52 203次阅读