>  기사  >  백엔드 개발  >  Go 함수 클로저의 기본 구현

Go 함수 클로저의 기본 구현

Go语言进阶学习
Go语言进阶学习앞으로
2023-07-25 15:18:341261검색

함수 클로저는 대부분의 독자에게 고급 용어가 아닙니다. 그렇다면 클로저란 무엇일까요? 다음은 Wiki의 정의에서 발췌한 내용입니다.

클로저는 환경과 함께 함수를 저장하는 레코드입니다. 환경은 함수의 각 자유 변수(로컬에서 사용되지만 정의된 변수)를 연결하는 매핑입니다. 클로저가 생성될 때 이름이 바인딩된 값 또는 참조가 포함된 엔클로징 범위).

간단히 말해서 클로저는 함수와 참조 환경으로 구성된 엔터티입니다. 구현 과정에서 클로저는 외부 함수를 호출하고 내부 함수를 반환하여 구현되는 경우가 많습니다. 그 중 참조 환경은 외부 함수(내부 함수에서는 사용되지만 외부 함수에서 정의됨)의 자유 변수 매핑을 의미합니다. 내부 함수는 외부 자유 변수를 도입하여 이러한 변수가 외부 함수의 환경을 벗어나더라도 해제되거나 삭제되지 않습니다. 반환된 내부 함수는 여전히 이 정보를 보유합니다.

Go 함수 클로저의 기본 구현

이 구절은 이해하기 쉽지 않을 수 있으므로 예를 들어보겠습니다.

 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

보시다시피 Go의 두 가지 기능(함수는 일급 시민이고 익명 함수를 지원함)을 사용하면 클로저 구현을 쉽게 할 수 있습니다.

위의 예에서 <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는 클로저 함수이고 변수 x는 참조입니다. 환경의 조합은 폐쇄 엔터티입니다.

<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>
이것은outer🎜🎜함수 내부 및 익명 함수 외부의 지역 변수입니다. 일반 함수 호출이 끝나면 🎜🎜x🎜🎜는 함수 스택이 파괴됨에 따라 파괴됩니다. 그러나 익명 함수의 참조로 인해 🎜🎜outer🎜🎜반환된 함수 객체는 항상 유지됩니다🎜 🎜x🎜🎜 변수. 이로 인해 클로저에 대한 모든 호출이 발생합니다. 🎜🎜closure🎜🎜, 🎜🎜x🎜🎜변수가 누적됩니다. 🎜🎜🎜🎜이것은 일반 함수 호출과 다릅니다: 지역 변수🎜🎜x🎜🎜이 기능을 따르지 않습니다. 통화가 종료되고 사라집니다. 그렇다면 왜 이런가요? 🎜🎜🎜🎜🎜🎜🎜

实现原理

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

 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中,闭包在底层是一个结构体对象,它包含了函数指针与自由变量。

Go 컴파일러의 이스케이프 분석 메커니즘은 클로저 객체를 힙에 할당하므로 함수 스택이 파괴될 때 자유 변수가 사라지지 않고 클로저 엔터티에 따라 항상 존재할 수 있습니다. 따라서 클로저 사용의 장점과 단점은 분명합니다. 클로저는 전역 변수 사용을 피하고 대신 메모리에 저장된 자유 변수를 오랫동안 유지할 수 있지만, 이러한 암시적 자유 변수 보유는 부적절하게 사용될 경우 문제를 일으키기 쉽습니다. 메모리 낭비 및 누출.

실제 프로젝트에서는 클로저 사용 시나리오가 많지 않습니다. 물론 코드에 클로저를 작성하는 경우(예: 작성한 콜백 함수가 클로저를 형성하는 경우) 주의해야 합니다. 그렇지 않으면 메모리 사용 문제로 인해 문제가 발생할 수 있습니다.

위 내용은 Go 함수 클로저의 기본 구현의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
이 기사는 Go语言进阶学习에서 복제됩니다. 침해가 있는 경우 admin@php.cn으로 문의하시기 바랍니다. 삭제

관련 기사

더보기