当前位置: 首页 > 科技观察

一道封闭式围棋题,面试官说自己答错了:面试别人也增长知识

时间:2023-03-16 22:28:20 科技观察

大家好,我是站长polarisxu。通常,闭包应该是JS面试的必答题。随着越来越多的语言支持函数式范式,闭包的问题也时常出现。Go语言也是如此。本文从一个问题中介绍了Go中的闭包。这是《GoLoversWeekly》第90期的主题。以下代码输出什么?packagemainimport"fmt"funcapp()func(string)string{t:="Hi"c:=func(bstring)string{t=t+""+breturnt}returnc}funcmain(){a:=app()b:=app()a("go")fmt.Println(b("All"))}这道题答对的人相当多:60%。不管你答对还是答错,如果在最后加一行代码:fmt.Println(a("All")),它会输出什么?我想看看你说得对不对。(提示:可以输出t的地址看看是怎么回事。)01什么是闭包维基百科对闭包的定义:在计算机科学中,闭包(英文:Closure),也称为词法闭包(LexicalClosure),或函数闭包,是一种在支持一等函数的编程语言中实现词法绑定的技术。闭包被实现为一个结构体,它存储了一个函数(通常是它的入口地址)和一个关联的环境(相当于一个符号查找表)。环境包含符号和值之间的几种对应关系,其中既包括约束变量(函数内部绑定的符号),也包括自由变量(在函数外部定义但在函数内部引用),有些函数也可能没有自由变量。闭包和函数最大的区别在于,当闭包被捕获时,它的自由变量会在捕获时确定,这样即使它脱离了捕获的上下文,它仍然可以照常运行。捕获时对值的处理可以是值拷贝,也可以是名称引用,这通常由语言设计者决定,也可能由用户指定(如C++)。关于(函数)闭包,有几个关键点:函数是一等公民;闭包所在的环境可以引用环境中的值;当被问到什么是闭包时,网上普遍的回答是:在支持函数中是一等公民的语言,一个函数的返回值是另一个函数,返回的函数可以访问父函数中的变量.当返回的函数在外部执行时,会产生一个闭包。所以在上面的问题中,函数app的返回值是另外一个函数,所以产生了一个闭包。02Go中的闭包Go中的函数是一等公民。之前写过一篇文章:函数是一等公民。这是什么意思?在日常开发中,闭包是很常见的。举几个例子。net/http包中标准库的函数ProxyURL实现如下://ProxyURLreturnssaproxyfunction(foruseinaTransport)//thatalwaysreturnsthesameURL.funcProxyURL(fixedURL*url.URL)func(*Request)(*url.URL,error){returnfunc(*Request)(*url.URL,error){returnfixedURL,nil}}它的返回值是另一个函数,签名为:func(*Request)(*url.URL,error)中返回的函数,父类引用了函数的参数fixedURL(ProxyURL),所以这是一个闭包。Web中间件在Web开发中,中间件一般使用闭包。比如Echo框架中的一个中间件://BasicAuthWithConfigreturnsanBasicAuthmiddlewarewithconfig.//See`BasicAuth()`.funcBasicAuthWithConfig(configBasicAuthConfig)echo.MiddlewareFunc{//Defaultsifconfig.Validator==nil{panic("echo:basic-authmiddlewarerequires"valid)...returnfunc(nextecho.HandlerFunc)echo.HandlerFunc{returnfunc(cecho.Context)error{///省略很多代码...}}}首先echo.MiddlewareFunc是一个函数:typeMiddlewareFuncfunc(HandlerFunc)HandlerFunc和echo。HandlerFunc也是一个函数:typeHandlerFuncfunc(Context)error所以,上面的函数嵌套了好几层,是一个典型的闭包。这是闭包吗?Go不支持函数嵌套定义,函数内嵌套的函数必须是匿名函数的形式。匿名函数在Go中很常见,比如启动一个goroutine,一般都是通过匿名函数来实现的。现在有个问题,下面的代码是闭包吗?packagemainimport("fmt")funcmain(){a:=5func(){fmt.Println("a=",a)}()}如果按照上面网上的一般答案,这不是闭包,因为有没有返回功能。但是根据维基百科的定义,这是一个闭包。还有其他证据吗?在Go语言规范中,关于函数字面量(匿名函数)有一句话:Functionliteralsareclosed:theymayrefertovariablesdefinedinasurroundingfunction。然后这些变量在周围函数和函数文字之间共享,只要它们可访问,它们就会存在。也就是说,函数字面量(匿名函数)是闭包,它们可以引用外部函数定义的变量。另外,官方FAQ中有这样的描述:Whathappenswithshutdownsrunningasgoroutines?例子是:done<-true}()}//waitforallgoroutinestocompletebeforeexitingfor_=rangevalues{<-done}}这是Go中很常见的代码(容易写错),FAQ上说启动goroutine的匿名函数是闭包。03编译看实现回到开头的话题,我们看一下编译,Go闭包的实现是不是按照维基百科,“闭包在实现上是一种结构,它存储了一个函数(通常是它的入口地址)和一个关联的环境(相当于一个符号查找表)”。$gotoolcompile-Smain.go看关键字代码:0x000000000(main.go:5)TEXT"".app(SB),ABIInternal,$24-80x000000000(main.go:5)MOVQ(TLS),CX0x000900009(main.go:5)CMPQSP,16(CX)0x000d00013(main.go:5)PCDATA$0,$-20x000d00013(main.go:5)JLS960x000f00015(main.go:5)PCDATA$0,$-10x000f00015(main.go:5)SUBQ$24,SP0x001300019(main.go:5)MOVQBP,16(SP)0x001800024(main.go:5)LEAQ16(SP),BP0x001d00029(main.go:5)FUNCDATA$0,gclocals·2a5305abe05176240e61b8620e19ad)8002(S1B002).go:5)FUNCDATA$1,gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)0x001d00029(main.go:7)LEAQtype.noalg.struct{Fuintptr;"".tstring}(SB),AX0x002400036(main.go:7)MOVQAX,(SP)0x002800040(main.go:7)PCDATA$1,$00x002800040(main.go:7)CALLruntime.newobject(SB)0x002d00045(main.go:7)MOVQ8(SP),AX0x003200050(main.go:7)LEAQ“”.app.func1(SB),CX0x003900057(main.go:7)MOVQCX,(AX)0x003c00060(main.go:7)MOVQ$2,16(AX)0x004400068(main.go:7)LEAQgo.string。“嗨”(SB),CX0x004b00075(main.go:7)MOVQCX,8(AX)0x004f00079(main.go:10)MOVQAX,"".~r0+32(SP)0x005400084(main.go:10)MOVQ16(SP),BP0x005900089(main.go:10)ADDQ$24,SP0x005d00093(main.go:10)RET0x005e00094(main.go:10)NOPwhereLEAQtype.noalg.struct{Fuintptr;".tstring}(SB),AX这一行展示了Go对闭包的实现和wiki维基百科说的类似现在看看下面是不是这样实现的:.go"".main.func1STEXTsize=215args=0x8locals=0x50funcid=0x00x000000000(test.go:9)TEXT"".main.func1(SB),ABIInternal,$80-80x000000000(test.go:9)MOVQ(TLS),CX0x000900009(test.go:9)CMPQSP,16(CX)0x000d00013(test.go:9)PCDATA$0,$-20x000d00013(test.go:9)JLS2050x001300019(test.go:9)PCDATA$0,$-10x001300019(test.go:9)SUBQ$80,SP0x001700023(test.go:9)MOVQBP,72(SP)0x001c00028(test.go:9)LEAQ72(SP),BP0x002100033(test.go:9)FUNCDATA$0,gclocals·69C1753BD5F81501D95132D08AF04464(SB)0x002100033(test..go:9)funcdata$1,gclocals·gclocals·9fb7f0986f647f17cb53ddaddada1484e0f7amoveytestymotymotymotymotymotymotymot+abt100+amotymotymotymotymotymotymotymotymot;(SP)0x002a00042(test.go:10)PCDATA$1,$00x002a00042(test.go:10)CALLruntime.convT64(SB)0x002f00047(test.go:10)MOVQ8(SP),AX0x003400052(test.go:10)MOVQAX,""..autotmp_21+64(SP)0x003900057(test.go:10)LEAQtype.[2]接口{}(SB),CX0x004000064(test.go:10)MOVQCX,(SP)0x004400068(test.go:10)PCDATA$1,$10x004400068(test.go:10)CALLruntime.newobject(SB)0x004900073(test.go:10)MOVQ8(SP),AX0x004e00078(test.go:10)LEAQtype.string(SB),CX0x005500085(test.go:10)MOVQCX,(AX)0x005800088(test.go:10)LEAQ""..stmp_1(SB),CX0x005f00095(test.go:10)MOVQCX,8(AX)0x006300099(test.go:10)LEAQtype.int(SB),CX0x006a00106(test.go:10)MOVQCX,16(AX)0x006e00110(test.go:10)PCDATA$0,$-20x006e00110(test.go:10)CMPLruntime.writeBarrier(SB),$00x007500117(test.go:10)JNE1890x007700119(test.go:10)MOVQ"..autotmp_21+64(SP),CX0x007c00124(test.go:10)MOVQCX,24(AX)0x008000128(test.go:10)PCDATA$0,$-10x008000128(test.go:10)PCDATA$1,$-1发现没有这个结构。可见Go对这种情况做了特殊处理,因为它不是可重用的匿名函数。04小结通过上面的讲解,应该对closurePackages有了更清晰的认识。如果你在面试中被问及闭包,你可以这样回答:对于闭包,函数必须是语言中的一等公民。一般来说,一个函数返回另一个函数,返回的函数可以引用外层函数的局部变量,这样就形成了一个闭包。通常,闭包被实现为存储函数和关联上下文的结构。但是在Go语言中,匿名函数就是一个闭包,可以直接引用外部函数的局部变量,因为Go规范和FAQ都是这么说的。面试官会不会被你吓一跳:原来如此,之前没有注意到后一种说法。本文转载自微信公众号「polarisxu」,可通过以下二维码关注。转载本文请联系polarisxu公众号。