0

Go语言之闭包篇

 1 year ago
source link: https://blog.gotocoding.com/archives/1786?amp%3Butm_medium=rss&%3Butm_campaign=go%25e8%25af%25ad%25e8%25a8%2580%25e4%25b9%258b%25e9%2597%25ad%25e5%258c%2585%25e7%25af%2587
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Go语言之闭包篇

在有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有颇多相似之处。

只是没有任何值捕获的闭包,在逃逸分析时可以做更多的优化。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK