本文介绍了以下内容:介绍了几个用GoLang语言编写的案例;介绍什么是闭包;介绍什么是闭包的延迟绑定;从闭包的延迟绑定到Go语言的GoRoutine1.几个有趣的案例开门见山,首先请看下面foo1()到foo7()的7个函数,然后回答下面的问题。(一次性抛出7个功能,还望见谅。不过每个函数都很短,本文将重点介绍这7个函数,请耐心看题,活跃脑细胞~)case1:funcfoo1(x*int)func(){returnfunc(){*x=*x+1fmt.Printf("foo1val=%d\n",*x)}}案例2:funcfoo2(xint)func(){returnfunc(){x=x+1fmt.Printf("foo2val=%d\n",x)}}case3:funcfoo3(){values:=[]int{1,2,3,5}for_,val:=范围值{fmt.Printf("foo3val=%d\n",val)}}案例4:funcshow(vinterface{}){fmt.Printf("foo4val=%v\n",v)}funcfoo4(){values:=[]int{1,2,3,5}for_,val:=rangevalues{goshow(val)}}case5:funcfoo5(){values:=[]int{1,2,3,5}for_,val:=rangevalues{gofunc(){fmt.Printf("foo5val=%v\n",val)}()}}案例6:varfoo6Chan=make(chanint,10)funcfoo6(){forval:=rangefoo6Chan{gofunc(){fmt.Printf("foo6val=%d\n",val)}()}}case7:funcfoo7(xint)[]func(){varfs[]func()values:=[]int{1,2,3,5}for_,val:=范围值{fs=append(fs,func(){fmt.打印("foo7val=%d\n",x+val)})}returnfs}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秒)向通道插入一组数据,此时foo6会打印什么?第三组实验:如果是微秒(10^-6秒)呢?毫秒(10^-3秒)呢?如果是秒呢?Q4:如果创建一个变量f7s=foo7(11),f7s是一个函数集合,遍历f7s会打印什么?接下来看看这些问题和对应的foo函数2.case1~2:Passingbyvalue(按值)vs.Passingbyreference(引用)副标题好难>0<...看case1和case2两组函数foo1()和foo2(),相信大家会知道其中一个知识点就是值传递和引用传递。事实上,Go并没有通过引用传递。即使foo1()在参数上加上*,内部实现机制依然是按值传递,传递的只是指针的值。但为了调用方便,下面将称为“引用传递”(为了区分正确的引用传递,这里加引号)。如下图,我们的目的是传递X变量,所以我们创建一个参数传递地址(临时地址变量),里面存放着X变量的地址值,在调用函数的时候把这个参数传递地址给它.功能怎么样?它将创建另一个入口地址,该地址是通行证地址的副本。该函数获取地址值,可以通过寻址获取X变量。这时,如果函数直接修改X变量,可以认为是对该变量值的“局部修改”或“永久修改”。”举个生活中的例子,比如一个叫“Function”的人要找一个叫“X”的人,Function来问我谁认识X,我拿出通讯录给他看X的家人。地址,函数的记忆力不太好,所以我拿了一个笔记本,把X的地址抄在了他自己的笔记本上。”在这个例子中,我的通讯录X的家庭地址是引用地址;函数复制了X的地址,即输入地址;X的家庭地址对应X变量的地址值。(咦,为什么你在说这样的细节?)这个话题在Golang中的“引用传递”好像有点扯远了,扯回来,我们来看看foo1()和foo2()。foo1()和foo2的区别()确实是值传递和引用传递,但这不是本文的中心,本文要介绍的内容在标题中已经说明:闭包(closure)。闭包(closure)什么是闭包?引用维基百科的定义:闭包是一个记录,将函数和环境一起存储。闭包是由函数及其关联的引用环境组成的实体。所以闭包的核心是:函数和环境。其实,问题这篇文章的标题已经可以在这里得到回答:Whatexac闭包包含什么?答案是:功能和环境。但相信有些读者还不清楚:什么功能?什么环境?函数是指闭包在实际实现时,往往是通过调用外部函数,返回其内部函数来实现的。内部函数可以是内部命名函数、匿名函数或lambda表达式。当用户拿到一个闭包的时候,也相当于拿到了内部函数,每次执行完闭包,就相当于执行了内部函数。Environment,维基百科说的是和它相关的参考环境(function),可以说解释的非常准确。具体来说,参考环境指的是实践中外部函数的环境,闭包在生成时保存/记录了外部函数的所有环境。但是这段话对于还没有理解闭包的同学还是不太友好,听完还是一头雾水。这里尝试做一个更实际的解释:如果外部函数的所有变量可见性都是局部的,即外部函数结束时生命周期结束,那么闭包的环境也关闭了。相反,闭包不再关闭,全局可见变量的修改也会影响闭包中的变量。跳回foo1()和foo2()示例只是为了解释闭包的功能和环境。funcfoo1(x*int)func(){returnfunc(){*x=*x+1fmt.Printf("foo1val=%d\n",*x)}}funcfoo2(xint)func(){returnfunc(){x=x+1fmt.Printf("foo1val=%d\n",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)。这里的f1\f2是闭包的函数,即foo1()\foo2()的内部匿名函数;而闭包的环境就是外部函数foo1()\foo2()的变量x(因为内部匿名函数指的是唯一相关的变量x,所以这里简化为变量x)。闭包函数的作用总结如下:1)。将环境变量x增加1;2).打印环境变量x。闭包的环境就是它的外部函数得到的变量x。因此,Q1第一组实验的答案是:f1()//foo1val=134f2()//foo2val=134f1()//foo1val=135f2()//foo2val=135这是因为闭包f1\f2在x=133时保存了整个环境,每次调用闭包f1\f2都会执行一个自增+打印的内部匿名函数。因此,第一个输出为(133+1=)134,第二个输出为(134+1=)135。那么Q1中第二组实验的答案呢?f1()//foo1val=234f2()//foo2val=136f1()//foo1val=235f2()//foo2val=137有趣的事情发生了!f1的值居然发生了很大的变化!通过这组实验可以更好的说明它的(函数)相关的引用环境其实就是生成闭包时外部函数的环境,所以变量x的可见性和作用域也和外部函数一样,又因为foo1是“按引用传递”,变量x的作用域并不局限于foo1(),所以当x发生变化时,闭包f1内部也会发生变化。这也是“相反,闭包不再关闭,全局可见变量的修改也会影响闭包中的变量”的证明。Q1第三组实验的答案:foo1(&x)()//foo1val=236foo2(x)()//foo2val=237foo1(&x)()//foo1val=237foo2(x)()//foo2val=238foo2(x)()//foo2val=238因为foo1()返回的闭包会修改变量x的值,所以调用foo1()()之后,变量x必须加1。foo2()返回的闭包只修改其内部环境的变量x,不影响调用外的变量x,而且每次调用foo2()返回的闭包是独立的,与调用foo2()的其他闭包无关,所以最后两次调用,打印的值是一样的;第一次调用和第二次调用foo2()发现打印的值都增加了1,因为两次调用之间传入的x值分别是236和237,而不是第二次对第一次加1时间,需要补充。3.case7:闭包的延迟绑定hhh,你以为我会继续讲case3,不过我先提到case7,惊喜不?废话不多说,我们来看看下面调用f7()时会打印什么?funcfoo7(xint)[]func(){varfs[]func()values:=[]int{1,2,3,5}for_,val:=rangevalues{fs=append(fs,func(){fmt.Printf("foo7val=%d\n",x+val)})}returnfs}//Q4实验:f7s:=foo7(11)for_,f7:=rangef7s{f7()}答案是:foo7val=16foo7val=16foo7val=16foo7val=16是的,你没有看错,会打印4行,而且都是16!是不是很惊喜!相信很多同学在网上都看过类似的案例,也都已经知道结果了。不清楚的同学现在也看到了答案。好吧,这就是著名的闭包延迟绑定问题。网上其实有很多解释。这里我尝试用之前闭包环境的定义来解释这个现象:“闭包是一个引用环境的函数和相关实体。在case7问题中,函数是打印变量val的值,引用环境就是变量val,正好这样的话,遍历到val=1的时候,不应该记录val=1的环境吗?上面闭包解释的最后,还有一句:闭包保存/记录所有环境外部函数在生成时的状态。就像普通变量/函数的定义和实际赋值/调用或执行一样,有两个阶段。闭包也是如此。在for循环内部,只声明一个闭包,foo7()返回的只是一个闭包的函数定义,只有在外部执行f7()的时候,闭包才真正执行,这时候闭包里面的变量才会被赋值。哎,如果是这样的话,应该'一个e异常被抛出?因为val是一个生命周期比foo7()短的变量?这就是闭包的神奇之处,它保存了相关引用的环境,即变量val,保证了闭包内的生命周期。所以,在执行这个闭包的时候,它会到外部环境去寻找最新的值!你不相信吗?来吧,我们写个临时案例,过几分钟执行一下就明白了:临时案例:funcfoo0()func(){x:=1f:=func(){fmt.Printf("foo0val=%d\n",x)}x=11returnf}foo0()()//猜猜我会输出什么?既然说了执行的时候会去外部环境找最新的值,那么x的最新值就是11,果然最后输出的是11。以上就是我通俗版本对闭包延迟绑定的解释。:)四。case3~6:GoRoutine的延迟绑定case3、case4、case5不是闭包。Case3只是遍历内部切片并打印出来。Case4在遍历过程中通过协程调用打印函数进行打印。Case5也是在遍历切片时调用了内部匿名函数print。Q2的case3问题答案先抛出:funcfoo3(){values:=[]int{1,2,3,5}for_,val:=rangevalues{fmt.Printf("foo3val=%d\n",val)}}foo3()//foo3val=1a//foo3val=2//foo3val=3//foo3val=5中规中矩,遍历其中的内容outputslice:1,2,3,5Q2的case4题答案又被抛出:funcshow(vinterface{}){fmt.Printf("foo4val=%v\n",v)}funcfoo4(){values:=[]int{1,2,3,5}for_,val:=rangevalues{goshow(val)}}foo4()//foo3val=2//foo3val=3//foo3val=1//foo3val=5um,因为GoRoutine的执行顺序是随机并行的,所以多次执行foo4()的输出顺序是不一样的,但是必须打印“1、2、3、5”的每个元素。最后Q2的case5题答案:funcfoo5(){values:=[]int{1,2,3,5}for_,val:=rangevalues{gofunc(){fmt.Printf("foo5val=%v\n",val)}()}}foo5()//foo3val=5//foo3val=5//foo3val=5//foo3val=5实际打印5,不意外,意外吗?!相信看过字幕的你不会感到惊讶(捂脸)。是的,接下来就要说说GoRoutine的延迟绑定了:其实这个问题的本质和闭包的延迟绑定是一样的,或者说,这个匿名函数的对象就是闭包。当我们调用gofunc(){xxx}()时,只要代码没有真正执行,它只是一个函数声明。当这个匿名函数被执行时,正是内部变量正在寻找真正赋值的时候。在case5中,for循环的遍历几乎是“瞬间”完成的,4个GoRoutines实际上是在它之后执行的。有没有发生冲突?这个时候for循环结束了,val的生命周期已经结束了。程序应该会报错吧?回想一下上一章,是不是一样的情况?是的,这个匿名函数不就是一个闭包吗?一切都说明了:闭包真正执行的时候,for循环结束,但是val的生命周期在闭包内部延长,赋给了最新的值5。不知道大家有没有好奇,既然GoRoutine的执行比for-loop慢,那么如果我在遍历的时候加一个sleep机制呢?所以我设计了Q3实验:varfoo6Chan=make(chanint,10)funcfoo6(){forval:=rangefoo6Chan{gofunc(){fmt.Printf("foo6val=%d\n",val)}()}}//Q3第一组实验gofoo6()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//第三组实验的微秒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)//secondfoo6Chan<-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),多执行几次试试。第一组答案如下:foo6val=5/3foo6val=5foo6val=5foo6val=5大部分时候执行为5第二组答案如下:foo6val=15/13/11/12foo6val=15/13foo6val=15foo6val=15大多数时候,结果是15。第三组答案如下://microsecondsfoo6val=23/21foo6val=23/22foo6val=25/23foo6val=25//毫秒foo6val=31foo6val=32foo6val=33foo6val=35//秒foo6val=41foo6val=42foo6val=43foo6val=45毫秒和秒这两组很确定,顺序输出。但不一定微妙,有时是顺序输出,大多数时候是随机输出如“22、22、23、25”或“21、22、25、25”。可以看出,GoRoutine的匿名函数从定义到执行,耗费了微妙的时间。所以我添加了一个临时案例来测试它到底需要多少。另一个临时情况:funcfoo8(){fori:=1;我<10;i++{curTime:=time.Now().UnixNano()gofunc(t1int64){t2:=time.Now().UnixNano()fmt.Printf("foo8ts=%dus\n",t2-t1)}(curTime)}}foo8()执行后发现耗时在5微秒到60微秒之间。不过以上实验数据均来自我的iMac笔记本,CPU为i7-7700K4.2GHz;我放在笔记本上(CPU是i5-8250U1.6GHz1.8GHz)跑起来,发现耗时0微秒!一开始怀疑是时间精度的问题,于是把t1和t2时间都打印出来,精度可以达到纳秒级。怀着不可置信的心情,我重新进行了第三组实验,每组都是顺序输出!好吧,回到我的iMac问题。现在只要记住一件事:GoRoutine的匿名函数延迟绑定的本质就是闭包。实际生成中注意这个写法~写在最后。闭包是很常见的事情,但在实际代码中并不推荐。如果你不小心写了内存泄漏,你是找不到的。特别是不要为了炫技而刻意写闭包,实在是没有必要。
