题目: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package  mainimport  "fmt" func  app () func (string ) string  {"Hi" func (b string ) string  {" "  + breturn  treturn  cfunc  main () "go" )"All" ))
A:Hi All;B:Hi go All;C:Hi;D:go All
这道题目答对的人蛮多的:60%。不管你是答对还是答错,如果最后再加一行代码:fmt.Println(a(“All”)),它输出什么?想看看你是不是蒙对了。(提示:你可以输出 t 的地址,看看是什么情况。)
答案:  闭包通过一个结构体来实现,它存储一个函数和一个关联的上下文环境。
	因此每一个闭包都有自己的上下文环境,所以a与b两个闭包的内容是互不干涉的。
	第一次调用a("go")时,将 “go”添加到 a 闭包的 t 变量中,此时 a 闭包的 t 变量为 “Hi go”。
	接着调用 b("All"),将 “All” 添加到 b 闭包的 t 变量中,此时 b 闭包的 t 变量为 “Hi All”。
	最后我们再次调用 a("All"),将 “All” 添加到 a 闭包的 t 变量中,此时 a 闭包的 t 变量为 “Hi go All” 。
	所以最后的答案为 fmt.Println(b("All"))输出 "Hi All",fmt.Println(a("All"))输出Hi go All。
以下解析来自 Go语言中文网 
01 什么是闭包 维基百科对闭包的定义 :
在计算机科学中,闭包(英语:Closure),又称词法闭包(Lexical Closure)或函数闭包(function closures),是在支持头等函数的编程语言中实现词法绑定的一种技术。闭包在实现上是一个结构体,它存储了一个函数(通常是其入口地址)和一个关联的环境(相当于一个符号查找表)。环境里是若干对符号和值的对应关系,它既要包括约束变量(该函数内部绑定的符号),也要包括自由变量(在函数外部定义但在函数内被引用),有些函数也可能没有自由变量。闭包跟函数最大的不同在于,当捕捉闭包的时候,它的自由变量会在捕捉时被确定,这样即便脱离了捕捉时的上下文,它也能照常运行。捕捉时对于值的处理可以是值拷贝,也可以是名称引用,这通常由语言设计者决定,也可能由用户自行指定(如 C++)。
 
关于(函数)闭包,有几个关键点:
函数是一等公民; 
闭包所处环境,可以引用环境里的值; 
 
问到什么是闭包时,网上一般这么回答的:
在支持函数是一等公民的语言中,一个函数的返回值是另一个函数,被返回的函数可以访问父函数内的变量,当这个被返回的函数在外部执行时,就产生了闭包。
 
所以,上面题目中,函数 app 的返回值是另一个函数,因此产生了闭包。
02 Go 中的闭包 Go 中的函数是一等公民,之前写过一篇文章:函数是一等公民,这到底在说什么? 
日常开发中,闭包是很常见的。举几个例子。
标准库 在 net/http 包中的函数 ProxyURL,实现如下:
1 2 3 4 5 6 7 func  ProxyURL (fixedURL *url.URL) func (*Request) error ) {return  func (*Request) error ) {return  fixedURL, nil 
它的返回值是另一个函数,签名是:
1 func (*Request) error )
在返回的函数中,引用了父函数(ProxyURL)的参数 fixedURL,因此这是闭包。
Web 中间件 在 Web 开发中,中间件一般都会使用闭包。比如 Echo 框架中的一个中间件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 func  BasicAuthWithConfig (config BasicAuthConfig) if  config.Validator == nil  {panic ("echo: basic-auth middleware requires a validator function" )return  func (next echo.HandlerFunc) return  func (c echo.Context) error  {
首先,echo.MiddlewareFunc 是一个函数:
1 type  MiddlewareFunc func (HandlerFunc) 
而 echo.HandlerFunc 也是一个函数:
1 type  HandlerFunc func (Context) error 
所以,上面的函数嵌套了几层,是典型的闭包。
这是闭包吗? 在 Go 中不支持函数嵌套定义,函数内嵌套函数,必须通过匿名函数的形式。匿名函数在 Go 中是很常见的,比如开启一个 goroutine,通常通过匿名函数。
现在有一个问题,以下代码是闭包吗?
1 2 3 4 5 6 7 8 9 10 11 12 package  mainimport  (  "fmt" func  main () 5 func () "a =" , a)
如果按照上面网上一般的回答,这不是闭包,因为并没有返回函数。但按照维基百科的定义,这个属于闭包。有没有其他证据呢?
在 Go 语言规范中,关于函数字面值(匿名函数)有这么一句话:
Function literals are closures : they may refer to variables defined in a surrounding function. Those variables are then shared between the surrounding function and the function literal, and they survive as long as they are accessible.
 
也就是说,函数字面值(匿名函数)是闭包,它们可以引用外层函数定义的变量。
此外,在官方 FAQ 中有这样的说明:
What happens with closures running as goroutines? 
例子是:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 func  main () make (chan  bool )string {"a" , "b" , "c" }for  _, v := range  values {go  func () true for  _ = range  values {
这是 Go 中很常见的代码(很容易写错的),FAQ 称开启 goroutine 的那个匿名函数是一个闭包。
03 汇编看看实现 回到开始的题目,我们通过汇编看看,Go 闭包的实现,是不是按照维基百科说的,「闭包在实现上是一个结构体,它存储了一个函数(通常是其入口地址)和一个关联的环境(相当于一个符号查找表)」。
1 $ go tool compile -S main.go
看关键代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 0x0000  00000  (main.go :5 )	TEXT	"" .app(SB), ABIInternal, $24 -8 0x0000  00000  (main.go :5 )	MOVQ	(TLS), CX0x0009  00009  (main.go :5 )	CMPQ	SP, 16 (CX)0x000d  00013  (main.go :5 )	PCDATA	$0 , $-2 0x000d  00013  (main.go :5 )	JLS	96 0x000f  00015  (main.go :5 )	PCDATA	$0 , $-1 0x000f  00015  (main.go :5 )	SUBQ	$24 , SP0x0013  00019  (main.go :5 )	MOVQ	BP, 16 (SP)0x0018  00024  (main.go :5 )	LEAQ	16 (SP), BP0x001d  00029  (main.go :5 )	FUNCDATA	$0 , gclocals·2 a5305abe05176240e61b8620e19a815(SB)0x001d  00029  (main.go :5 )	FUNCDATA	$1 , gclocals·33 cdeccccebe80329f1fdbee7f5874cb(SB)0x001d  00029  (main.go :7 )	LEAQ	type .noalg.struct  { F uintptr ; "" .t string  }(SB), AX0x0024  00036  (main.go :7 )	MOVQ	AX, (SP)0x0028  00040  (main.go :7 )	PCDATA	$1 , $0 0x0028  00040  (main.go :7 )	CALL	runtime.newobject(SB)0x002d  00045  (main.go :7 )	MOVQ	8 (SP), AX0x0032  00050  (main.go :7 )	LEAQ	"" .app.func1(SB), CX0x0039  00057  (main.go :7 )	MOVQ	CX, (AX)0x003c  00060  (main.go :7 )	MOVQ	$2 , 16 (AX)0x0044  00068  (main.go :7 )	LEAQ	go .string ."Hi" (SB), CX0x004b  00075  (main.go :7 )	MOVQ	CX, 8 (AX)0x004f  00079  (main.go :10 )	MOVQ	AX, "" .~r0+32 (SP)0x0054  00084  (main.go :10 )	MOVQ	16 (SP), BP0x0059  00089  (main.go :10 )	ADDQ	$24 , SP0x005d  00093  (main.go :10 )	RET0x005e  00094  (main.go :10 )	NOP
其中 LEAQ type.noalg.struct { F uintptr; "".t string }(SB), AX 这行表明 Go 对闭包的实现和维基百科说的类似。
现在看看下面这种是不是这么实现的:
1 2 3 4 5 6 7 8 9 10 11 12 package  mainimport  (  "fmt" func  main () 5 func () "a =" , a)
看看汇编
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 $ go tool compile -S test.go"" .main.func1 STEXT size=215 args=0x8 locals=0x50 funcid=0x0"" .main.func1(SB), ABIInternal, $80 -8$0 , $-2$0 , $-1$80 , SP$0 , gclocals·69c1753bd5f81501d95132d08af04464(SB)$1 , gclocals·9fb7f0986f647f17cb53dda1484e0f7a(SB)"" .a+88(SP), AX$1 , $0 "" ..autotmp_21+64(SP)type .[2]interface {}(SB), CX$1 , $1 "" ..stmp_1(SB), CX$0 , $-2$0 "" ..autotmp_21+64(SP), CX$0 , $-1$1 , $-1
发现并没有这样的结构体,可见 Go 对这种情况做了特殊处理,因为它不是重复使用的匿名函数。
04 总结 通过以上的讲解,对闭包应该有了更清晰的认识。如果面试中再被问到闭包,你可以这么回答:
对闭包来说,函数在该语言中得是一等公民。一般来说,一个函数返回另外一个函数,这个被返回的函数可以引用外层函数的局部变量,这形成了一个闭包。通常,闭包通过一个结构体来实现,它存储一个函数和一个关联的上下文环境。但 Go 语言中,匿名函数就是一个闭包,它可以直接引用外部函数的局部变量,因为 Go 规范和 FAQ 都这么说了。
 
面试官会不会被你惊到:原来如此,后一种说法我之前没有注意过。
4 月 14 日更新:
来自微信公众号 的读者 **gopher **留言:
noalg 代表不会生成 equal 和 hash 函数,因为闭包的 struct 是匿名的,不存在比较或者作为 key 的场景。
 
答案解析来自:https://polarisxu.studygolang.com/posts/go/action/go-closure/