在有GC和闭包实现的语言中,我最熟悉的是Lua语言。所以在使用Go语言时,碰到不熟悉的细节,总是会以Lua的机制来对比。
然而由于动态语言和静态语言的区别(静态语言总是有更多优化的机制), 以至于很多时候会得出错误的结论。
比如下面代码:
package main
import "os"
func exist(list []int, f func(n int)bool) bool {
for _, n := range list {
if f(n) == true {
return true
}
}
return false
}
func main() {
count := len(os.Args)
a := make([]int, 0)
for i := 0; i < count; i++ {
a = append(a, i)
}
exist(a, func(n int) bool {
return n == (count - 1)
})
}
这段代码定义了一个闭包,然后作为参数传给exist函数。
按照Lua的经验,定义闭包肯定是需要malloc内存。然而Go语言反手就教我做人。
使用go run -gcflags="-m -l" a.go
可以发现,这个闭包并没有被分配在堆上。
再使用go tool compile -N -l -S a.go
来看一下与闭包相关的Plan9汇编代码。
"".exist STEXT size=234 args=0x28 locals=0x58
................
0x0085 00133 (a.go:5) MOVQ "".f+120(SP), DX
0x008a 00138 (a.go:5) MOVQ AX, (SP)
0x008e 00142 (a.go:5) MOVQ (DX), AX
0x0091 00145 (a.go:5) PCDATA $1, $1
0x0091 00145 (a.go:5) CALL AX
0x0093 00147 (a.go:5) MOVBLZX 8(SP), AX
0x0098 00152 (a.go:5) MOVB AL, ""..autotmp_5+23(SP)
0x009c 00156 (a.go:5) NOP
0x00a0 00160 (a.go:5) TESTB AL, AL
0x00a2 00162 (a.go:5) JNE 166
0x00a4 00164 (a.go:5) JMP 184
0x00a6 00166 (a.go:6) MOVB $1, "".~r2+128(SP)
0x00ae 00174 (a.go:6) MOVQ 80(SP), BP
0x00b3 00179 (a.go:6) ADDQ $88, SP
0x00b7 00183 (a.go:6) RET
0x00b8 00184 (a.go:5) PCDATA $1, $-1
0x00b8 00184 (a.go:5) JMP 186
0x00ba 00186 (a.go:5) JMP 188
"".main STEXT size=372 args=0x0 locals=0x90
................
0x00ff 00255 (a.go:17) XORPS X0, X0
0x0102 00258 (a.go:17) MOVUPS X0, ""..autotmp_4+88(SP)
0x0107 00263 (a.go:17) LEAQ ""..autotmp_4+88(SP), AX
0x010c 00268 (a.go:17) MOVQ AX, ""..autotmp_6+104(SP)
0x0111 00273 (a.go:17) TESTB AL, (AX)
0x0113 00275 (a.go:17) LEAQ "".main.func1(SB), CX
0x011a 00282 (a.go:17) MOVQ CX, ""..autotmp_4+88(SP)
0x011f 00287 (a.go:17) TESTB AL, (AX)
0x0121 00289 (a.go:17) MOVQ "".count+72(SP), AX
0x0126 00294 (a.go:17) MOVQ AX, ""..autotmp_4+96(SP)
0x012b 00299 (a.go:17) MOVQ "".a+112(SP), AX
0x0130 00304 (a.go:17) MOVQ "".a+120(SP), CX
0x0135 00309 (a.go:17) MOVQ "".a+128(SP), DX
0x013d 00317 (a.go:17) MOVQ AX, (SP)
0x0141 00321 (a.go:17) MOVQ CX, 8(SP)
0x0146 00326 (a.go:17) MOVQ DX, 16(SP)
0x014b 00331 (a.go:17) MOVQ ""..autotmp_6+104(SP), AX
0x0150 00336 (a.go:17) MOVQ AX, 24(SP)
0x0155 00341 (a.go:17) CALL "".exist(SB)
"".main.func1 STEXT nosplit size=54 args=0x10 locals=0x10
0x0000 00000 (a.go:17) TEXT "".main.func1(SB), NOSPLIT|NEEDCTXT|ABIInternal, $16-16
0x0000 00000 (a.go:17) SUBQ $16, SP
0x0004 00004 (a.go:17) MOVQ BP, 8(SP)
0x0009 00009 (a.go:17) LEAQ 8(SP), BP
0x000e 00014 (a.go:17) FUNCDATA $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
0x000e 00014 (a.go:17) FUNCDATA $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
0x000e 00014 (a.go:17) MOVQ 8(DX), AX
0x0012 00018 (a.go:17) MOVQ AX, "".count(SP)
0x0016 00022 (a.go:17) MOVB $0, "".~r1+32(SP)
0x001b 00027 (a.go:18) MOVQ "".count(SP), AX
0x001f 00031 (a.go:18) DECQ AX
0x0022 00034 (a.go:18) CMPQ "".n+24(SP), AX
0x0027 00039 (a.go:18) SETEQ "".~r1+32(SP)
0x002c 00044 (a.go:18) MOVQ 8(SP), BP
0x0031 00049 (a.go:18) ADDQ $16, SP
0x0035 00053 (a.go:18) RET
上面的代码并不算太复杂,我们大致可以翻译出他的等价Go语言(翻译出来的代码是可以被编译运行的)。
package main
import "os"
type Closure1 struct {
F func(int) bool
n int
}
var DX *Closure1
func func1(n int) bool {
x := DX.n - 1
return x == n
}
func exist(list []int, f *Closure1) bool {
for _, n := range list {
DX = f
if f.F(n) == true {
return true
}
}
return false
}
func main() {
count := len(os.Args)
a := make([]int, 0)
for i := 0; i < count; i++ {
a = append(a, i)
}
c := &Closure1{
F: func1,
n: count,
}
exist(a, c)
}
从上面的Go代码可以很清楚的看到,其实一个闭包到底分配不分配内存,关键就在于Closure1在栈上还是在堆上。
当Closure1结构暴露出来之后,一切都是那么的显然。
即然闭包是一个struct对象,那么Go当然可以和一般的自定义struct一样进行逃逸分析,而根据逃逸规则,这里的c
对象显然不需要逃逸。
一切都很完美,只是还有一个问题没有解决。
exist
在调用f函数时,是如何区分调用的是闭包还是非闭包,比如下面代码:
package main
import "os"
func exist(list []int, f func(n int)bool) bool {
for _, n := range list {
if f(n) == true {
return true
}
}
return false
}
func foo(n int) bool {
return n == 3
}
func main() {
count := len(os.Args)
a := make([]int, 0)
for i := 0; i < count; i++ {
a = append(a, i)
}
exist(a, foo)
}
再来看一下对应的汇编代码:
"".exist STEXT size=234 args=0x28 locals=0x58
.......
0x0085 00133 (a.go:5) MOVQ "".f+120(SP), DX
0x008a 00138 (a.go:5) MOVQ AX, (SP)
0x008e 00142 (a.go:5) MOVQ (DX), AX
0x0091 00145 (a.go:5) PCDATA $1, $1
0x0091 00145 (a.go:5) CALL AX
0x0093 00147 (a.go:5) MOVBLZX 8(SP), AX
0x0098 00152 (a.go:5) MOVB AL, ""..autotmp_5+23(SP)
0x009c 00156 (a.go:5) NOP
0x00a0 00160 (a.go:5) TESTB AL, AL
0x00a2 00162 (a.go:5) JNE 166
0x00a4 00164 (a.go:5) JMP 184
.......
"".main STEXT size=300 args=0x0 locals=0x78
0x00ea 00234 (a.go:20) MOVQ "".a+88(SP), AX
0x00ef 00239 (a.go:20) MOVQ "".a+96(SP), CX
0x00f4 00244 (a.go:20) MOVQ "".a+104(SP), DX
0x00f9 00249 (a.go:20) MOVQ AX, (SP)
0x00fd 00253 (a.go:20) MOVQ CX, 8(SP)
0x0102 00258 (a.go:20) MOVQ DX, 16(SP)
0x0107 00263 (a.go:20) LEAQ "".foo·f(SB), AX
0x010e 00270 (a.go:20) MOVQ AX, 24(SP)
0x0113 00275 (a.go:20) CALL "".exist(SB)
通过对比可以发现,其实exist函数的代码并没有任何变化,有变化的代码是a.go
第20行。
再来将汇编翻译成Go语言:
package main
import "os"
type Closure1 struct {
F func(int) bool
}
var DX *Closure1
func foo(n int) bool {
return n == 3
}
func exist(list []int, f *Closure1) bool {
for _, n := range list {
DX = f
if f.F(n) == true {
return true
}
}
return false
}
func main() {
count := len(os.Args)
a := make([]int, 0)
for i := 0; i < count; i++ {
a = append(a, i)
}
c := &Closure1{
F: foo,
}
exist(a, c)
}
通过对比两次翻译后的Go语言,可以发现一件很有意思的事。
Go语言其实把所有函数都抽象成闭包,这一点倒是与Lua有颇多相似之处。
只是没有任何值捕获的闭包,在逃逸分析时可以做更多的优化。