클로저에 대해 실제로 이야기하기 전에 몇 가지 기초를 다져 봅시다:
[관련 권장 사항: Go 비디오 튜토리얼]
1.1 전제 지식
함수형 프로그래밍은 문제를 보는 프로그래밍 패러다임입니다. 모든 함수는 작은 함수를 사용하여 더 큰 함수로 구성되도록 설계되었습니다. 함수의 매개변수도 함수입니다. 함수에 의해 반환되는 함수도 함수입니다. 우리의 일반적인 프로그래밍 패러다임은 다음과 같습니다.
함수형 프로그래밍은 객체지향 프로그래밍의 반대라고 볼 수 있습니다. 일반적으로 일부 프로그래밍 언어만 특정 프로그래밍 방법을 강조합니다. 대부분의 언어는 다중 패러다임 언어이며 여러 가지를 지원할 수 있습니다. JavaScript, Go 등과 같은 다양한 프로그래밍 방법
함수형 프로그래밍은 컴퓨터 연산을 함수의 계산으로 보는 사고방식입니다. 사실 함수형 프로그래밍에 대해 이야기한 다음 클로저에 대해 이야기해야 합니다. 왜냐하면 클로저 자체가 함수이기 때문입니다. 형식 프로그래밍의 특징
함수형 프로그래밍에서 함수는 일급 객체입니다. 즉, 함수는 다른 함수의 입력 매개변수 값으로 사용될 수 있고, 함수에서 값을 반환할 수도 있고, 수정되거나 할당될 수도 있음을 의미합니다. 변수. (위키피디아)
일반적으로 순수 함수형 프로그래밍 언어는 프로그램 상태와 가변 객체의 직접적인 사용을 허용하지 않습니다. 함수형 프로그래밍 자체는 공유 상태, 변수 상태 사용을 피하고 부작용을 피하는 것입니다. 가능해요 .
함수형 프로그래밍에는 일반적으로 다음과 같은 특징이 있습니다.
함수는 일급 시민입니다. 함수는 먼저 배치되고 매개변수로 사용될 수 있고, 값을 할당할 수 있고, 전달될 수 있으며, 반환 값으로 사용될 수 있습니다.
부작용 없음: 함수는 완전히 독립적으로 유지되어야 하며 외부 변수의 값을 수정할 수 없고 외부 상태를 수정하지 않습니다.
참조 투명성: 함수 연산은 외부 변수나 상태에 의존하지 않습니다. 동일한 입력 매개변수를 사용하면 어떤 경우에도 반환 값이 동일해야 합니다.
Scope(범위), 프로그래밍 개념 일반적으로 프로그램 코드에 사용되는 이름은 항상 유효/사용 가능한 것은 아니며 이를 제한합니다. 이름의 가용성은 이름의 범위입니다.
일반인의 용어로 함수 범위는 함수가 작동할 수 있는 범위를 나타냅니다. 함수는 상자와 비슷합니다. 범위는 닫힌 상자, 즉 상자 내부에서만 사용할 수 있고 독립적인 범위가 되는 함수의 지역 변수로 이해할 수 있습니다.
함수 내 지역 변수가 함수를 떠난 후 범위 밖으로 튀어나와 변수를 찾을 수 없습니다. (외부 함수의 범위에는 내부 함수가 포함되므로 내부 함수는 외부 함수의 지역 변수를 사용할 수 있습니다.) 예를 들어 다음 innerTmep
出了函数作用域就找不到该变量,但是 outerTemp
은 내부 함수에서도 사용할 수 있습니다.
어떤 언어이든 기본적으로 사용되지 않는 메모리 공간을 재활용하는 특정 메모리 재활용 메커니즘이 있습니다. 재활용 메커니즘은 일반적으로 위에서 언급한 기능의 범위와 관련이 있으며 로컬 변수가 그 역할을 합니다. . 도메인이 재활용될 수 있습니다. 계속 참조되는 경우 재활용되지 않습니다.
소위 범위 상속이란 앞서 언급한 작은 상자가 외부 큰 상자의 범위를 상속받을 수 있다는 의미입니다. 작은 상자에서는 큰 상자의 것들을 직접 가져갈 수 있습니다. 하지만 큰 상자 안에 있는 물건은 탈출이 발생하지 않는 한 꺼낼 수 없습니다. (탈출은 작은 상자에 있는 물건이 참고 사항을 제공하는 것으로 이해될 수 있으며, 큰 상자는 받자마자 사용할 수 있습니다. ). 일반적으로 변수 범위에는 두 가지 유형이 있습니다.
전역 범위: 어디서나 작동
로컬 범위: 일반적으로 코드 블록, 함수, 패키지, 내부 함수선언/정의 변수를 로컬 변수라고 합니다. , 그리고 범위는 함수 내부로 제한됩니다
1.2 클로저의 정의
"대부분의 경우 먼저 이해하고 정의하는 것이 아니라 먼저 정의하고 이해하는 경우가 많습니다." , 먼저 정의해 봅시다. 이해하지 못해도 상관없습니다:
클로저는 함수와 주변 환경(어휘 환경, 어휘 환경)에 대한 번들 참조의 조합입니다 . 즉, 클로저를 사용하면 개발자가 내부 함수에서 외부 함수의 범위에 액세스할 수 있습니다. 클로저는 함수가 생성될 때 생성됩니다.
한 문장으로 설명:
Go 언어라는 단어는 위의 정의에서 찾을 수 없습니다. 똑똑한 학생들은 클로저가 언어와 아무런 관련이 없다는 것을 알아야 합니다. 이는 JavaScript나 Go에만 해당되는 것이 아니라 기능적 프로그래밍 언어에만 해당됩니다. 맞습니다. 함수형 프로그래밍을 지원하는 모든 언어는 클로저를 지원하며, Go와 JavaScript는 그 중 두 가지입니다. 현재 버전의 Java도 클로저를 지원하지만 일부 사람들은 이것이 완벽한 클로저가 아니라고 생각할 수도 있습니다. 자세한 내용은 본문에서 설명합니다. . sum() 메서드는 외부 함수 lazySum()
의 매개변수와 지역 변수를 참조하고 에서 <code>sum() 함수를 반환할 수 있습니다. lazySum()
code>에 해당하는 매개변수와 변수는 반환된 함수에 저장되며 나중에 호출할 수 있습니다.
count
를 여러 번 추가합니다. import "fmt" func main() { sumFunc := lazySum([]int{1, 2, 3, 4, 5}) fmt.Println("等待一会") fmt.Println("结果:", sumFunc()) } func lazySum(arr []int) func() int { fmt.Println("先获取函数,不求结果") var sum = func() int { fmt.Println("求结果...") result := 0 for _, v := range arr { result = result + v } return result } return sum }
count
횟수가 변경되나요? count
는 당연히 외부 함수의 로컬 변수인데, 메모리 함수 참조(번들링)에서는 내부 함수가 노출되어 실행됩니다. 결과는 다음과 같습니다. 先获取函数,不求结果 等待一会 求结果... 结果: 15
count
입니다. 실제로 이 상황을 요약하면 다음과 같습니다. lazySum()
이 한 번 생성되어 세 번 실행되었다는 내용이 있는데, 세 번 실행하면 이렇게 됩니다. 다를 것입니다. 창조해 보세요. 어떤 모습일까요? 실험: import "fmt" func main() { sumFunc := lazySum([]int{1, 2, 3, 4, 5}) fmt.Println("等待一会") fmt.Println("结果:", sumFunc()) fmt.Println("结果:", sumFunc()) fmt.Println("结果:", sumFunc()) }func lazySum(arr []int) func() int { fmt.Println("先获取函数,不求结果") count := 0 var sum = func() int { count++ fmt.Println("第", count, "次求结果...") result := 0 for _, v := range arr { result = result + v } return result } return sum }
先获取函数,不求结果 等待一会 第 1 次求结果... 结果: 15 第 2 次求结果... 结果: 15 第 3 次求结果... 结果: 15
클로저가 생성되면 참조된 외부 변수 개수
는 이미 1개의 사본이 생성되었습니다. 즉, 각각 호출해도 상관없습니다
sum()
方法可以引用外部函数 lazySum()
的参数以及局部变量,在lazySum()
返回函数 sum()
的时候,相关的参数和变量都保存在返回的函数中,可以之后再进行调用。
上面的函数或许还可以更进一步,体现出捆绑函数和其周围的状态,我们加上一个次数 count
:
import "fmt" func main() { sumFunc := lazySum([]int{1, 2, 3, 4, 5}) fmt.Println("等待一会") fmt.Println("结果:", sumFunc()) sumFunc1 := lazySum([]int{1, 2, 3, 4, 5}) fmt.Println("等待一会") fmt.Println("结果:", sumFunc1()) sumFunc2 := lazySum([]int{1, 2, 3, 4, 5}) fmt.Println("等待一会") fmt.Println("结果:", sumFunc2()) }func lazySum(arr []int) func() int { fmt.Println("先获取函数,不求结果") count := 0 var sum = func() int { count++ fmt.Println("第", count, "次求结果...") result := 0 for _, v := range arr { result = result + v } return result } return sum }
上面代码输出什么呢?次数 count
会不会发生变化,count
明显是外层函数的局部变量,但是在内存函数引用(捆绑),内层函数被暴露出去了,执行结果如下:
先获取函数,不求结果 等待一会 第 1 次求结果... 结果: 15 先获取函数,不求结果 等待一会 第 1 次求结果... 结果: 15 先获取函数,不求结果 等待一会 第 1 次求结果... 结果: 15
结果是 count
其实每次都会变化,这种情况总结一下:
此时有人可能有疑问了,前面是lazySum()
被创建了 1 次,执行了 3 次,但是如果是 3 次执行都是不同的创建,会是怎么样呢?实验一下:
import "fmt" func main() { sumFunc, productSFunc := lazyCalculate([]int{1, 2, 3, 4, 5}) fmt.Println("等待一会") fmt.Println("结果:", sumFunc()) fmt.Println("结果:", productSFunc()) }func lazyCalculate(arr []int) (func() int, func() int) { fmt.Println("先获取函数,不求结果") count := 0 var sum = func() int { count++ fmt.Println("第", count, "次求加和...") result := 0 for _, v := range arr { result = result + v } return result } var product = func() int { count++ fmt.Println("第", count, "次求乘积...") result := 0 for _, v := range arr { result = result * v } return result } return sum, product }
执行的结果如下,每次执行都是第 1 次:
先获取函数,不求结果 等待一会 第 1 次求加和... 结果: 15 第 2 次求乘积... 结果: 0
从以上的执行结果可以看出:
闭包被创建的时候,引用的外部变量count
就已经被创建了 1 份,也就是各自调用是没有关系的。
继续抛出一个问题,**如果一个函数返回了两个函数,这是一个闭包还是两个闭包呢?**下面我们实践一下:
一次返回两个函数,一个用于计算加和的结果,一个计算乘积:
import "fmt" func main() { i := 0 testFunc := test(&i) testFunc() fmt.Printf("outer i = %d\n", i) }func test(i *int) func() { *i = *i + 1 fmt.Printf("test inner i = %d\n", *i) return func() { *i = *i + 1 fmt.Printf("func inner i = %d\n", *i) } }
运行结果如下:
test inner i = 1 func inner i = 2 outer i = 2
从上面结果可以看出,闭包是函数返回函数的时候,不管多少个返回值(函数),都是一次闭包,如果返回的函数有使用外部函数变量,则会绑定到一起,相互影响:
闭包绑定了周围的状态,我理解此时的函数就拥有了状态,让函数具有了对象所有的能力,函数具有了状态。
上面的例子,我们闭包中用到的都是数值,如果我们传递指针,会是怎么样的呢?
func main() { i := 0 testFunc := test(&i) testFunc() fmt.Printf("outer i address %v\n", &i) } func test(i *int) func() { *i = *i + 1 fmt.Printf("test inner i address %v\n", i) return func() { *i = *i + 1 fmt.Printf("func inner i address %v\n", i) } }
运行结果如下:
test inner i address 0xc0003fab98 func inner i address 0xc0003fab98 outer i address 0xc0003fab98
可以看出如果是指针的话,闭包里面修改了指针对应的地址的值,也会影响闭包外面的值。这个其实很容易理解,Go 里面没有引用传递,只有值传递,那我们传递指针的时候,也是值传递,这里的值是指针的数值(可以理解为地址值)。
当我们函数的参数是指针的时候,参数会拷贝一份这个指针地址,当做参数进行传递,因为本质还是地址,所以内部修改的时候,仍然可以对外部产生影响。
闭包里面的数据其实地址也是一样的,下面的实验可以证明:
func main() { i := 0 testFunc := test(&i) i = i + 100 fmt.Printf("outer i before testFunc %d\n", i) testFunc() fmt.Printf("outer i after testFunc %d\n", i) }func test(i *int) func() { *i = *i + 1 fmt.Printf("test inner i = %d\n", *i) return func() { *i = *i + 1 fmt.Printf("func inner i = %d\n", *i) } }
输出如下, 因此可以推断出,闭包如果引用外部环境的指针数据,只是会拷贝一份指针地址数据,而不是拷贝一份真正的数据(==先留个问题:拷贝的时机是什么时候呢==):
test inner i = 1 outer i before testFunc 101 func inner i = 102 outer i after testFunc 102
上面的例子仿佛都在告诉我们,闭包创建的时候,数据就已经拷贝了,但是真的是这样么?
下面是继续前面的实验:
import "fmt" func main() { sumFunc := lazySum([]int{1, 2, 3, 4, 5}) fmt.Println("等待一会") fmt.Println("结果:", sumFunc()) } func lazySum(arr []int) func() int { fmt.Println("先获取函数,不求结果") count := 0 var sum = func() int { fmt.Println("第", count, "次求结果...") result := 0 for _, v := range arr { result = result + v } return result } count = count + 100 return sum }
我们在创建闭包之后,把数据改了,之后执行闭包,答案肯定是真实影响闭包的执行,因为它们都是指针,都是指向同一份数据:
等待一会 第 100 次求结果... 结果: 15
假设我们换个写法,让闭包外部环境中的变量在声明闭包函数的之后,进行修改:
func main() { funcs := testFunc(100) for _, v := range funcs { v() } } func testFunc(x int) []func() { var funcs []func() values := []int{1, 2, 3} for _, val := range values { funcs = append(funcs, func() { fmt.Printf("testFunc val = %d\n", x+val) }) } return funcs }
实际执行结果,count
会是修改后的值:
testFunc val = 103 testFunc val = 103 testFunc val = 103
这也证明了,实际上闭包并不会在声明var sum = func() int {...}
这句话之后,就将外部环境的 count
계속 질문하세요. **함수가 두 개의 함수를 반환하는 경우 이는 하나의 클로저인가요, 아니면 두 개의 클로저인가요? **아래에서 연습해 보세요: 한 번에 두 개의 함수를 반환합니다. 하나는 합의 결과를 계산하는 데 사용되고 다른 하나는 곱을 계산하는 데 사용됩니다.
import ( "fmt" "time" ) func main() { sumFunc := lazySum([]int{1, 2, 3, 4, 5}) fmt.Println("等待一会") fmt.Println("结果:", sumFunc()) time.Sleep(time.Duration(3) * time.Second) fmt.Println("结果:", sumFunc()) } func lazySum(arr []int) func() int { fmt.Println("先获取函数,不求结果") count := 0 var sum = func() int { count++ fmt.Println("第", count, "次求结果...") result := 0 for _, v := range arr { result = result + v } return result } go func() { time.Sleep(time.Duration(1) * time.Second) count = count + 100 fmt.Println("go func 修改后的变量 count:", count) }() return sum }
先获取函数,不求结果 等待一会 第 1 次求结果... 结果: 15 go func 修改后的变量 count: 101 第 102 次求结果... 结果: 15
import "fmt" func testFunc(i int) func() int { i = i * 2 testFunc := func() int { i++ return i } i = i * 2 return testFunc } func main() { test := testFunc(1) fmt.Println(test()) }
5
go build --gcflags=-m main.go
go tool compile -N -l -S main.go
"".testFunc STEXT size=218 args=0x8 locals=0x38 funcid=0x0 align=0x0 0x0000 00000 (main.go:5) TEXT "".testFunc(SB), ABIInternal, -8 0x0000 00000 (main.go:5) CMPQ SP, 16(R14) 0x0004 00004 (main.go:5) PCDATA rrreee, $-2 0x0004 00004 (main.go:5) JLS 198 0x000a 00010 (main.go:5) PCDATA rrreee, $-1 0x000a 00010 (main.go:5) SUBQ , SP 0x000e 00014 (main.go:5) MOVQ BP, 48(SP) 0x0013 00019 (main.go:5) LEAQ 48(SP), BP 0x0018 00024 (main.go:5) FUNCDATA rrreee, gclocals·69c1753bd5f81501d95132d08af04464(SB) 0x0018 00024 (main.go:5) FUNCDATA , gclocals·d571c0f6cf0af59df28f76498f639cf2(SB) 0x0018 00024 (main.go:5) FUNCDATA , "".testFunc.arginfo1(SB) 0x0018 00024 (main.go:5) MOVQ AX, "".i+64(SP) 0x001d 00029 (main.go:5) MOVQ rrreee, "".~r0+16(SP) 0x0026 00038 (main.go:5) LEAQ type.int(SB), AX 0x002d 00045 (main.go:5) PCDATA , rrreee 0x002d 00045 (main.go:5) CALL runtime.newobject(SB) 0x0032 00050 (main.go:5) MOVQ AX, "".&i+40(SP) 0x0037 00055 (main.go:5) MOVQ "".i+64(SP), CX 0x003c 00060 (main.go:5) MOVQ CX, (AX) 0x003f 00063 (main.go:6) MOVQ "".&i+40(SP), CX 0x0044 00068 (main.go:6) MOVQ "".&i+40(SP), DX 0x0049 00073 (main.go:6) MOVQ (DX), DX 0x004c 00076 (main.go:6) SHLQ , DX 0x004f 00079 (main.go:6) MOVQ DX, (CX) 0x0052 00082 (main.go:7) LEAQ type.noalg.struct { F uintptr; "".i *int }(SB), AX 0x0059 00089 (main.go:7) PCDATA , 0x0059 00089 (main.go:7) CALL runtime.newobject(SB) 0x005e 00094 (main.go:7) MOVQ AX, ""..autotmp_3+32(SP) 0x0063 00099 (main.go:7) LEAQ "".testFunc.func1(SB), CX 0x006a 00106 (main.go:7) MOVQ CX, (AX) 0x006d 00109 (main.go:7) MOVQ ""..autotmp_3+32(SP), CX 0x0072 00114 (main.go:7) TESTB AL, (CX) 0x0074 00116 (main.go:7) MOVQ "".&i+40(SP), DX 0x0079 00121 (main.go:7) LEAQ 8(CX), DI 0x007d 00125 (main.go:7) PCDATA rrreee, $-2 0x007d 00125 (main.go:7) CMPL runtime.writeBarrier(SB), rrreee 0x0084 00132 (main.go:7) JEQ 136 0x0086 00134 (main.go:7) JMP 142 0x0088 00136 (main.go:7) MOVQ DX, 8(CX) 0x008c 00140 (main.go:7) JMP 149 0x008e 00142 (main.go:7) CALL runtime.gcWriteBarrierDX(SB) 0x0093 00147 (main.go:7) JMP 149 0x0095 00149 (main.go:7) PCDATA rrreee, $-1 0x0095 00149 (main.go:7) MOVQ ""..autotmp_3+32(SP), CX 0x009a 00154 (main.go:7) MOVQ CX, "".testFunc+24(SP) 0x009f 00159 (main.go:11) MOVQ "".&i+40(SP), CX 0x00a4 00164 (main.go:11) MOVQ "".&i+40(SP), DX 0x00a9 00169 (main.go:11) MOVQ (DX), DX 0x00ac 00172 (main.go:11) SHLQ , DX 0x00af 00175 (main.go:11) MOVQ DX, (CX) 0x00b2 00178 (main.go:12) MOVQ "".testFunc+24(SP), AX 0x00b7 00183 (main.go:12) MOVQ AX, "".~r0+16(SP) 0x00bc 00188 (main.go:12) MOVQ 48(SP), BP 0x00c1 00193 (main.go:12) ADDQ , SP 0x00c5 00197 (main.go:12) RET 0x00c6 00198 (main.go:12) NOP 0x00c6 00198 (main.go:5) PCDATA , $-1 0x00c6 00198 (main.go:5) PCDATA rrreee, $-2 0x00c6 00198 (main.go:5) MOVQ AX, 8(SP) 0x00cb 00203 (main.go:5) CALL runtime.morestack_noctxt(SB) 0x00d0 00208 (main.go:5) MOVQ 8(SP), AX 0x00d5 00213 (main.go:5) PCDATA rrreee, $-1 0x00d5 00213 (main.go:5) JMP 0
count< /code>는 수정된 값이 됩니다. 🎜rrreee🎜이것은 또한 실제로 <code>var sum = func() int {...}</를 선언한 후 클로저가 외부 환경의 <code>를 변경하지 않는다는 것을 증명합니다. code> count
는 클로저에 바인딩되지만 함수가 클로저 함수를 반환할 때 바인딩됩니다. 이것이 🎜지연 바인딩🎜입니다. 🎜如果还没看明白没关系,我们再来一个例子:
func main() { funcs := testFunc(100) for _, v := range funcs { v() } } func testFunc(x int) []func() { var funcs []func() values := []int{1, 2, 3} for _, val := range values { funcs = append(funcs, func() { fmt.Printf("testFunc val = %d\n", x+val) }) } return funcs }
上面的例子,我们闭包返回的是函数数组,本意我们想入每一个 val
都不一样,但是实际上 val
都是一个值,==也就是执行到return funcs
的时候(或者真正执行闭包函数的时候)才绑定的 val
值==(关于这一点,后面还有个Demo可以证明),此时 val
的值是最后一个 3
,最终输出结果都是 103
:
testFunc val = 103 testFunc val = 103 testFunc val = 103
以上两个例子,都是闭包延迟绑定的问题导致,这也可以说是 feature,到这里可能不少同学还是对闭包绑定外部变量的时机有疑惑,到底是返回闭包函数的时候绑定的呢?还是真正执行闭包函数的时候才绑定的呢?
下面的例子可以有效的解答:
import ( "fmt" "time" ) func main() { sumFunc := lazySum([]int{1, 2, 3, 4, 5}) fmt.Println("等待一会") fmt.Println("结果:", sumFunc()) time.Sleep(time.Duration(3) * time.Second) fmt.Println("结果:", sumFunc()) } func lazySum(arr []int) func() int { fmt.Println("先获取函数,不求结果") count := 0 var sum = func() int { count++ fmt.Println("第", count, "次求结果...") result := 0 for _, v := range arr { result = result + v } return result } go func() { time.Sleep(time.Duration(1) * time.Second) count = count + 100 fmt.Println("go func 修改后的变量 count:", count) }() return sum }
输出结果如下:
先获取函数,不求结果 等待一会 第 1 次求结果... 结果: 15 go func 修改后的变量 count: 101 第 102 次求结果... 结果: 15
第二次执行闭包函数的时候,明显 count
被里面的 go func()
修改了,也就是调用的时候,才真正的获取最新的外部环境,但是在声明的时候,就会把环境预留保存下来。
其实本质上,Go Routine的匿名函数的延迟绑定就是闭包的延迟绑定,上面的例子中,go func(){}
获取到的就是最新的值,而不是原始值0
。
总结一下上面的验证点:
2.1 好处
纯函数没有状态,而闭包则是让函数轻松拥有了状态。但是凡事都有两面性,一旦拥有状态,多次调用,可能会出现不一样的结果,就像是前面测试的 case 中一样。那么问题来了:
Q:如果不支持闭包的话,我们想要函数拥有状态,需要怎么做呢?
A: 需要使用全局变量,让所有函数共享同一份变量。
但是我们都知道全局变量有以下的一些特点(在不同的场景,优点会变成缺点):
闭包可以一定程度优化这个问题:
除了以上的好处,像在 JavaScript 中,没有原生支持私有方法,可以靠闭包来模拟私有方法,因为闭包都有自己的词法环境。
2.2 坏处
函数拥有状态,如果处理不当,会导致闭包中的变量被误改,但这是编码者应该考虑的问题,是预期中的场景。
闭包中如果随意创建,引用被持有,则无法销毁,同时闭包内的局部变量也无法销毁,过度使用闭包会占有更多的内存,导致性能下降。一般而言,能共享一份闭包(共享闭包局部变量数据),不需要多次创建闭包函数,是比较优雅的方式。
从上面的实验中,我们可以知道,闭包实际上就是外部环境的逃逸,跟随着闭包函数一起暴露出去。
我们用以下的程序进行分析:
import "fmt" func testFunc(i int) func() int { i = i * 2 testFunc := func() int { i++ return i } i = i * 2 return testFunc } func main() { test := testFunc(1) fmt.Println(test()) }
执行结果如下:
5
先看看逃逸分析,用下面的命令行可以查看:
go build --gcflags=-m main.go
可以看到 变量 i
被移到堆中,也就是本来是局部变量,但是发生逃逸之后,从栈里面放到堆里面,同样的 test()
函数由于是闭包函数,也逃逸到堆上。
下面我们用命令行来看看汇编代码:
go tool compile -N -l -S main.go
生成代码比较长,我截取一部分:
"".testFunc STEXT size=218 args=0x8 locals=0x38 funcid=0x0 align=0x0 0x0000 00000 (main.go:5) TEXT "".testFunc(SB), ABIInternal, $56-8 0x0000 00000 (main.go:5) CMPQ SP, 16(R14) 0x0004 00004 (main.go:5) PCDATA $0, $-2 0x0004 00004 (main.go:5) JLS 198 0x000a 00010 (main.go:5) PCDATA $0, $-1 0x000a 00010 (main.go:5) SUBQ $56, SP 0x000e 00014 (main.go:5) MOVQ BP, 48(SP) 0x0013 00019 (main.go:5) LEAQ 48(SP), BP 0x0018 00024 (main.go:5) FUNCDATA $0, gclocals·69c1753bd5f81501d95132d08af04464(SB) 0x0018 00024 (main.go:5) FUNCDATA $1, gclocals·d571c0f6cf0af59df28f76498f639cf2(SB) 0x0018 00024 (main.go:5) FUNCDATA $5, "".testFunc.arginfo1(SB) 0x0018 00024 (main.go:5) MOVQ AX, "".i+64(SP) 0x001d 00029 (main.go:5) MOVQ $0, "".~r0+16(SP) 0x0026 00038 (main.go:5) LEAQ type.int(SB), AX 0x002d 00045 (main.go:5) PCDATA $1, $0 0x002d 00045 (main.go:5) CALL runtime.newobject(SB) 0x0032 00050 (main.go:5) MOVQ AX, "".&i+40(SP) 0x0037 00055 (main.go:5) MOVQ "".i+64(SP), CX 0x003c 00060 (main.go:5) MOVQ CX, (AX) 0x003f 00063 (main.go:6) MOVQ "".&i+40(SP), CX 0x0044 00068 (main.go:6) MOVQ "".&i+40(SP), DX 0x0049 00073 (main.go:6) MOVQ (DX), DX 0x004c 00076 (main.go:6) SHLQ $1, DX 0x004f 00079 (main.go:6) MOVQ DX, (CX) 0x0052 00082 (main.go:7) LEAQ type.noalg.struct { F uintptr; "".i *int }(SB), AX 0x0059 00089 (main.go:7) PCDATA $1, $1 0x0059 00089 (main.go:7) CALL runtime.newobject(SB) 0x005e 00094 (main.go:7) MOVQ AX, ""..autotmp_3+32(SP) 0x0063 00099 (main.go:7) LEAQ "".testFunc.func1(SB), CX 0x006a 00106 (main.go:7) MOVQ CX, (AX) 0x006d 00109 (main.go:7) MOVQ ""..autotmp_3+32(SP), CX 0x0072 00114 (main.go:7) TESTB AL, (CX) 0x0074 00116 (main.go:7) MOVQ "".&i+40(SP), DX 0x0079 00121 (main.go:7) LEAQ 8(CX), DI 0x007d 00125 (main.go:7) PCDATA $0, $-2 0x007d 00125 (main.go:7) CMPL runtime.writeBarrier(SB), $0 0x0084 00132 (main.go:7) JEQ 136 0x0086 00134 (main.go:7) JMP 142 0x0088 00136 (main.go:7) MOVQ DX, 8(CX) 0x008c 00140 (main.go:7) JMP 149 0x008e 00142 (main.go:7) CALL runtime.gcWriteBarrierDX(SB) 0x0093 00147 (main.go:7) JMP 149 0x0095 00149 (main.go:7) PCDATA $0, $-1 0x0095 00149 (main.go:7) MOVQ ""..autotmp_3+32(SP), CX 0x009a 00154 (main.go:7) MOVQ CX, "".testFunc+24(SP) 0x009f 00159 (main.go:11) MOVQ "".&i+40(SP), CX 0x00a4 00164 (main.go:11) MOVQ "".&i+40(SP), DX 0x00a9 00169 (main.go:11) MOVQ (DX), DX 0x00ac 00172 (main.go:11) SHLQ $1, DX 0x00af 00175 (main.go:11) MOVQ DX, (CX) 0x00b2 00178 (main.go:12) MOVQ "".testFunc+24(SP), AX 0x00b7 00183 (main.go:12) MOVQ AX, "".~r0+16(SP) 0x00bc 00188 (main.go:12) MOVQ 48(SP), BP 0x00c1 00193 (main.go:12) ADDQ $56, SP 0x00c5 00197 (main.go:12) RET 0x00c6 00198 (main.go:12) NOP 0x00c6 00198 (main.go:5) PCDATA $1, $-1 0x00c6 00198 (main.go:5) PCDATA $0, $-2 0x00c6 00198 (main.go:5) MOVQ AX, 8(SP) 0x00cb 00203 (main.go:5) CALL runtime.morestack_noctxt(SB) 0x00d0 00208 (main.go:5) MOVQ 8(SP), AX 0x00d5 00213 (main.go:5) PCDATA $0, $-1 0x00d5 00213 (main.go:5) JMP 0
可以看到闭包函数实际上底层也是用结构体new
创建出来的:
힙에서 i
사용: i
:
也就是返回函数的时候,实际上返回结构体,结构体里面记录了函数的引用环境。
4.1 Java 支不支持闭包?
网上有很多种看法,实际上 Java 虽然暂时不支持返回函数作为返参,但是Java 本质上还是实现了闭包的概念的,所使用的的方式是内部类的形式,因为是内部类,所以相当于自带了一个引用环境,算是一种不完整的闭包。
目前有一定限制,比如是 final
즉, 함수를 반환할 때 실제로 함수의 참조 환경을 기록한 구조체를 반환합니다.
4.1 Java는 클로저를 지원하나요? 사실 Java는 현재 반환 함수를 반환 매개변수로 지원하지 않지만 Java는 기본적으로 클로저 개념을 구현하고 사용하는 메소드를 내부 클래스 형태로 사용하기 때문에 많은 의견이 있습니다. 내부 클래스이므로 불완전한 폐쇄로 간주되는 참조 환경을 가져오는 것과 같습니다.
현재 특정 제한 사항이 있습니다. 예를 들어 final
로 선언되거나 명확하게 정의된 값이 전달될 수 있는 경우에만 가능합니다.
Stack Overflow에 관련 답변이 있습니다:stackoverflow.com/questions /5…
🎜Wiki의 내용은 다음과 같습니다. 🎜🎜🎜함수형 프로그래밍은 오랫동안 학계에서 인기가 있었지만 산업용 애플리케이션은 거의 없습니다. 이러한 상황이 발생하는 가장 큰 이유는 함수형 프로그래밍이 CPU 및 메모리 리소스를 심각하게 소모한다고 간주되는 경우가 많기 때문입니다[🎜18]🎜 이는 함수형 프로그래밍 언어의 초기 구현에서 효율성 문제를 고려하지 않았으며, 함수형 프로그래밍의 특성 때문입니다. 예를 들어 🎜참조 투명성🎜 등을 보장하려면 고유한 데이터 구조와 알고리즘이 필요합니다. [🎜19]🎜🎜🎜그러나 최근에는 상업용 또는 산업용 시스템에서 여러 기능적 프로그래밍 언어가 사용되었습니다 [🎜20]🎜. 예: 🎜[
- Erlang은 스웨덴 회사인 Ericsson이 1980년대 후반에 개발했으며 원래는 내결함성 통신 시스템을 구현하는 데 사용되었습니다. 이후 Nortel, Facebook, Électricité de France 및 WhatsApp과 같은 회사에서 일련의 응용 프로그램을 만드는 데 인기 있는 언어로 사용되었습니다. [21][22]
- Scheme은 초기 Apple Macintosh 컴퓨터에서 여러 응용 프로그램의 기반으로 사용되었으며 최근에는 훈련 시뮬레이션 소프트웨어 및 망원경 제어 방향 등에 적용되고 있습니다. .
- OCaml은 1990년대 중반에 출시되어 재무 분석, 운전자 검증, 산업용 로봇 프로그래밍, 임베디드 소프트웨어의 정적 분석 등의 분야에서 상용 응용 프로그램을 찾았습니다.
- Haskell은 원래 연구용 언어로 사용되었지만 항공우주 시스템, 하드웨어 설계, 네트워크 프로그래밍과 같은 분야의 여러 회사에서도 사용되었습니다.산업에서 사용되는 다른 기능적 프로그래밍 언어에는 다중 파라디가
스칼라23] , f#, wolfram language , common lisp, standard ml 및 clojure가 포함됩니다. 잠깐. 개인적인 관점에서는 순수 함수형 프로그래밍에 대해 낙관적이지는 않지만, 앞으로는 거의 모든 고급 프로그래밍에 필요한 것이 함수형 프로그래밍에 대한 아이디어를 갖게 될 것이라고 믿습니다. 특히 Java 수용을 기대합니다. 함수형 프로그래밍. 내가 아는 언어 중에서 Go와 JavaScript의 함수형 프로그래밍 기능은 개발자들에게 깊은 사랑을 받고 있습니다(물론 버그를 작성하면 싫어할 것입니다).
요즘 갑자기 인기를 얻는 이유도 세상이 계속 발전하고 메모리가 점점 커지고 있기 때문입니다. 이 요소의 한계가 거의 해방되었습니다.
세상은 다채롭다고 생각합니다. 한 가지가 세상을 지배하는 것은 절대 불가능합니다. 프로그래밍 언어나 프로그래밍 패러다임에도 마찬가지입니다. 미래에는 결국 역사는 인류 사회의 발전에 맞는 것들을 걸러낼 것입니다.
더 많은 프로그래밍 관련 지식을 보려면
프로그래밍 비디오를 방문하세요! !
위 내용은 Golang의 클로저에 대한 간략한 분석의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!