Maison  >  Article  >  développement back-end  >  L'implémentation sous-jacente de la fermeture de la fonction Go

L'implémentation sous-jacente de la fermeture de la fonction Go

Go语言进阶学习
Go语言进阶学习avant
2023-07-25 15:18:341296parcourir

La fermeture de fonction n'est pas un vocabulaire avancé pour la plupart des lecteurs, alors qu'est-ce qu'une fermeture ? Voici un extrait de la définition sur Wiki :

une fermeture est un enregistrement stockant une fonction avec un environnement. L'environnement est une cartographie associant chaque variable libre de la fonction (variables utilisées localement, mais définies dans une portée englobante) avec la valeur ou la référence à laquelle le nom était lié lors de la création de la fermeture.

En bref, une fermeture est une entité composée d'une fonction et d'un environnement de référence. Au cours du processus d'implémentation, les fermetures sont souvent implémentées en appelant des fonctions externes et en renvoyant leurs fonctions internes. Parmi eux, l'environnement de référence fait référence au mappage de variables libres dans la fonction externe (utilisées par la fonction interne, mais définies dans la fonction externe). La fonction interne introduit des variables libres externes afin que ces variables ne soient pas libérées ou supprimées même si elles quittent l'environnement de la fonction externe. La fonction interne renvoyée contient toujours ces informations.

L'implémentation sous-jacente de la fermeture de la fonction Go

Ce passage n'est peut-être pas facile à comprendre, alors utilisons simplement un exemple.

 1package main
 2
 3import "fmt"
 4
 5func outer() func() int {
 6    x := 1
 7    return func() int {
 8        x++
 9        return x
10    }
11}
12
13func main() {
14    closure := outer()
15    fmt.Println(closure())
16    fmt.Println(closure())
17}
18
19// output
202
213

Comme vous pouvez le constater, deux fonctionnalités de Go (les fonctions sont des citoyens de première classe et la prise en charge des fonctions anonymes) facilitent la mise en œuvre de fermetures.

Dans l'exemple ci-dessus, <code style="font-size: inherit;line-height: inherit;overflow-wrap: break-word;padding: 2px 4px;border-radius: 4px;margin-right: 2px;margin-left: 2px;color: rgb(233, 105, 0);background: rgb(248, 248, 248);"><span style="font-size: 15px;letter-spacing: 1px;">closure</span>是闭包函数,变量x就是引用环境,它们的组合就是闭包实体。<span style="font-size: 15px;letter-spacing: 1px;">x</span>本是<span style="font-size: 15px;letter-spacing: 1px;">outer</span>函数之内,匿名函数之外的局部变量。在正常函数调用结束之后,<span style="font-size: 15px;letter-spacing: 1px;">x</span>就会随着函数栈的销毁而销毁。但是由于匿名函数的引用,<span style="font-size: 15px;letter-spacing: 1px;">outer</span>返回的函数对象会一直持有<span style="font-size: 15px;letter-spacing: 1px;">x</span>变量。这造成了每次调用闭包<code style="font-size: inherit;line-height: inherit;overflow-wrap: break-word;padding: 2px 4px;border-radius: 4px;margin-right: 2px;margin-left: 2px;color: rgb(233, 105, 0);background: rgb(248, 248, 248);"><span style="font-size: 15px;letter-spacing: 1px;">closure</span><span style="font-size: 15px;letter-spacing: 1px;">x</span>变量都会得到累加。

这里和普通的函数调用不一样:局部变量<span style="font-size: 15px;letter-spacing: 1px;">x</span>closure est la fonction de fermeture, et la variable x est la référence environnement. Leur combinaison est une entité de fermeture.

<p style="font-size: inherit;color: inherit;line-height: inherit;margin-top: 1.5em;margin-bottom: 1.5em;">x<span style="font-size: 15px;letter-spacing: 1px;"></span></p>
C'estouter🎜🎜Variables locales dans les fonctions et en dehors des fonctions anonymes. Une fois l'appel de fonction normal terminé, 🎜🎜x🎜🎜 sera détruit au fur et à mesure que la pile de fonctions est détruite. Mais en raison de la référence de la fonction anonyme, 🎜🎜outer🎜🎜L'objet fonction renvoyé sera toujours conservé🎜 🎜x🎜🎜 variable. Cela se traduit par chaque appel à la fermeture 🎜🎜closure🎜🎜, 🎜🎜x🎜🎜les variables seront accumulées. 🎜🎜🎜🎜Ceci est différent des appels de fonction ordinaires : variables locales🎜🎜x🎜🎜 ne suit pas La fonction l'appel se termine et disparaît. Alors, pourquoi est-ce ? 🎜🎜🎜🎜🎜🎜🎜

实现原理

我们不妨从汇编入手,将上述代码稍微修改一下

 1package main
 2
 3func outer() func() int {
 4    x := 1
 5    return func() int {
 6        x++
 7        return x
 8    }
 9}
10
11func main() {
12    _ := outer()
13}

得到编译后的汇编语句如下。

 1$ go tool compile -S -N -l main.go 
 2"".outer STEXT size=181 args=0x8 locals=0x28
 3        0x0000 00000 (main.go:3)        TEXT    "".outer(SB), ABIInternal, $40-8
 4        ...
 5        0x0021 00033 (main.go:3)        MOVQ    $0, "".~r0+48(SP)
 6        0x002a 00042 (main.go:4)        LEAQ    type.int(SB), AX
 7        0x0031 00049 (main.go:4)        MOVQ    AX, (SP)
 8        0x0035 00053 (main.go:4)        PCDATA  $1, $0
 9        0x0035 00053 (main.go:4)        CALL    runtime.newobject(SB)
10        0x003a 00058 (main.go:4)        MOVQ    8(SP), AX
11        0x003f 00063 (main.go:4)        MOVQ    AX, "".&x+24(SP)
12        0x0044 00068 (main.go:4)        MOVQ    $1, (AX)
13        0x004b 00075 (main.go:5)        LEAQ    type.noalg.struct { F uintptr; "".x *int }(SB), AX
14        0x0052 00082 (main.go:5)        MOVQ    AX, (SP)
15        0x0056 00086 (main.go:5)        PCDATA  $1, $1
16        0x0056 00086 (main.go:5)        CALL    runtime.newobject(SB)
17        0x005b 00091 (main.go:5)        MOVQ    8(SP), AX
18        0x0060 00096 (main.go:5)        MOVQ    AX, ""..autotmp_4+16(SP)
19        0x0065 00101 (main.go:5)        LEAQ    "".outer.func1(SB), CX
20        0x006c 00108 (main.go:5)        MOVQ    CX, (AX)
21        ...

首先,我们发现不一样的是 <span style="font-size: 15px;letter-spacing: 1px;">x:=1</span> 会调用 <span style="font-size: 15px;letter-spacing: 1px;">runtime.newobject</span> 函数(内置<span style="font-size: 15px;letter-spacing: 1px;">new</span>函数的底层函数,它返回数据类型指针)。在正常函数局部变量的定义时,例如

 1package main
 2
 3func add() int {
 4    x := 100
 5    x++
 6    return x
 7}
 8
 9func main() {
10    _ = add()
11}

我们能发现 <span style="font-size: 15px;letter-spacing: 1px;">x:=100</span> 是不会调用 <span style="font-size: 15px;letter-spacing: 1px;">runtime.newobject</span> 函数的,它对应的汇编是如下

1"".add STEXT nosplit size=58 args=0x8 locals=0x10
2        0x0000 00000 (main.go:3)        TEXT    "".add(SB), NOSPLIT|ABIInternal, $16-8
3        ...
4        0x000e 00014 (main.go:3)        MOVQ    $0, "".~r0+24(SP)
5        0x0017 00023 (main.go:4)        MOVQ    $100, "".x(SP)  // x:=100
6        0x001f 00031 (main.go:5)        MOVQ    $101, "".x(SP)
7        0x0027 00039 (main.go:6)        MOVQ    $101, "".~r0+24(SP)
8        ...

留着疑问,继续往下看。我们发现有以下语句

1        0x004b 00075 (main.go:5)        LEAQ    type.noalg.struct { F uintptr; "".x *int }(SB), AX
2        0x0052 00082 (main.go:5)        MOVQ    AX, (SP)
3        0x0056 00086 (main.go:5)        PCDATA  $1, $1
4        0x0056 00086 (main.go:5)        CALL    runtime.newobject(SB)

我们看到 <span style="font-size: 15px;letter-spacing: 1px;">type.noalg.struct { F uintptr; "".x *int }(SB)</span>,这其实就是定义的一个闭包数据类型,它的结构表示如下

1type closure struct {
2    F uintptr   // 函数指针,代表着内部匿名函数
3    x *int      // 自由变量x,代表着对外部环境的引用
4}

之后,在通过 <span style="font-size: 15px;letter-spacing: 1px;">runtime.newobject</span> 函数创建了闭包对象。而且由于 <span style="font-size: 15px;letter-spacing: 1px;">LEAQ xxx yyy</span>代表的是将 <span style="font-size: 15px;letter-spacing: 1px;">xxx</span> 指针,传递给 <span style="font-size: 15px;letter-spacing: 1px;">yyy</span>,因此 <span style="font-size: 15px;letter-spacing: 1px;">outer</span> 函数最终的返回,其实是闭包结构体对象指针。在《详解逃逸分析》一文中,我们详细地描述了Go编译器的逃逸分析机制,对于这种函数返回暴露给外部指针的情况,很明显,闭包对象会被分配至堆上,变量x也会随着对象逃逸至堆。这就很好地解释了为什么<span style="font-size: 15px;letter-spacing: 1px;">x</span>变量没有随着函数栈的销毁而消亡。

我们可以通过逃逸分析来验证我们的结论

 1$  go build -gcflags &#39;-m -m -l&#39; main.go
 2# command-line-arguments
 3./main.go:6:3: outer.func1 capturing by ref: x (addr=true assign=true width=8)
 4./main.go:5:9: func literal escapes to heap:
 5./main.go:5:9:   flow: ~r0 = &{storage for func literal}:
 6./main.go:5:9:     from func literal (spill) at ./main.go:5:9
 7./main.go:5:9:     from return func literal (return) at ./main.go:5:2
 8./main.go:4:2: x escapes to heap:
 9./main.go:4:2:   flow: {storage for func literal} = &x:
10./main.go:4:2:     from func literal (captured by a closure) at ./main.go:5:9
11./main.go:4:2:     from x (reference) at ./main.go:6:3
12./main.go:4:2: moved to heap: x                   // 变量逃逸
13./main.go:5:9: func literal escapes to heap       // 函数逃逸

至此,我相信读者已经明白为什么闭包能持续持有外部变量的原因了。那么,我们来思考上文中留下的疑问,为什么在<span style="font-size: 15px;letter-spacing: 1px;">x:=1</span> 时会调用 <span style="font-size: 15px;letter-spacing: 1px;">runtime.newobject</span> 函数。

我们将上文中的例子改为如下,即删掉 <span style="font-size: 15px;letter-spacing: 1px;">x++</span> 代码

 1package main
 2
 3func outer() func() int {
 4    x := 1
 5    return func() int {
 6        return x
 7    }
 8}
 9
10func main() {
11    _ = outer()
12}

此时,<span style="font-size: 15px;letter-spacing: 1px;">x:=1</span>处的汇编代码,将不再调用 <span style="font-size: 15px;letter-spacing: 1px;">runtime.newobject</span> 函数,通过逃逸分析也会发现将<span style="font-size: 15px;letter-spacing: 1px;">x</span>不再逃逸,生成的闭包对象中的<span style="font-size: 15px;letter-spacing: 1px;">x</span>的将是值类型<span style="font-size: 15px;letter-spacing: 1px;">int</span>

1type closure struct {
2    F uintptr 
3    x int      
4}

这其实就是Go编译器做得精妙的地方:当闭包内没有对外部变量造成修改时,Go 编译器会将自由变量的引用传递优化为直接值传递,避免变量逃逸。


总结

函数闭包一点也不神秘,它就是函数和引用环境而组合的实体。在Go中,闭包在底层是一个结构体对象,它包含了函数指针与自由变量。

Le mécanisme d'analyse d'échappement du compilateur Go allouera l'objet de fermeture au tas, de sorte que la variable libre ne disparaisse pas lorsque la pile de fonctions est détruite, et qu'elle puisse toujours exister en fonction de l'entité de fermeture. Par conséquent, les avantages et les inconvénients de l'utilisation des fermetures sont évidents : les fermetures peuvent éviter d'utiliser des variables globales et conserver à la place des variables libres stockées en mémoire pendant une longue période. Cependant, cette conservation implicite des variables libres entraînera des problèmes en cas d'utilisation incorrecte ; gaspillage et fuite de mémoire.

Dans les projets réels, il n'existe pas beaucoup de scénarios d'utilisation des fermetures. Bien sûr, si vous écrivez une fermeture dans votre code, par exemple, une fonction de rappel que vous écrivez forme une fermeture, vous devez être prudent, sinon le problème d'utilisation de la mémoire pourrait vous causer des problèmes.

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Déclaration:
Cet article est reproduit dans:. en cas de violation, veuillez contacter admin@php.cn Supprimer