Golang의 클로저에 대한 간략한 분석

青灯夜游
풀어 주다: 2022-11-21 20:36:03
앞으로
6500명이 탐색했습니다.

Golang의 클로저에 대한 간략한 분석

1. 클로저란?

클로저에 대해 실제로 이야기하기 전에 몇 가지 기초를 다져 봅시다:

  • 함수 프로그래밍
  • 함수 범위
  • 범위의 상속 관계

[관련 권장 사항: Go 비디오 튜토리얼]

1.1 전제 지식

1.2.1 함수형 프로그래밍

함수형 프로그래밍은 문제를 보는 프로그래밍 패러다임입니다. 모든 함수는 작은 함수를 사용하여 더 큰 함수로 구성되도록 설계되었습니다. 함수의 매개변수도 함수입니다. 함수에 의해 반환되는 함수도 함수입니다. 우리의 일반적인 프로그래밍 패러다임은 다음과 같습니다.

  • 명령형 프로그래밍:
    • 주요 아이디어는 컴퓨터가 실행하는 단계에 주의를 기울이는 것입니다. 즉, 컴퓨터에게 먼저 무엇을 해야 할지, 그 다음에 무엇을 해야 할지 단계별로 알려줍니다.
    • 먼저 문제 해결 단계를 표준화하고 이를 특정 알고리즘으로 추상화한 다음 이를 구현하기 위한 특정 알고리즘을 작성합니다. 일반적으로 해당 언어가 절차적 프로그래밍 패러다임을 지원하는 한 이를 절차적 프로그래밍 언어라고 부를 수 있습니다. BASIC, C 등과 같은
  • 선언적 프로그래밍:
    • 주요 아이디어는 컴퓨터에게 무엇을 해야 하는지 알려주지만 웹 프로그래밍을 위한 SQL, HTML, CSS와 같이 수행 방법을 지정하지는 않는다는 것입니다.
  • 함수형 프로그래밍:
    • 어떻게 할지보다 무엇을 할지에만 집중하는 선언적 프로그래밍의 흔적이 있지만 "함수가 먼저다"라는 원칙에 더 중점을 둡니다. 즉, 함수가 나타날 수 있습니다. 장소, 매개변수, 변수, 반환값 등

함수형 프로그래밍은 객체지향 프로그래밍의 반대라고 볼 수 있습니다. 일반적으로 일부 프로그래밍 언어만 특정 프로그래밍 방법을 강조합니다. 대부분의 언어는 다중 패러다임 언어이며 여러 가지를 지원할 수 있습니다. JavaScript, Go 등과 같은 다양한 프로그래밍 방법

함수형 프로그래밍은 컴퓨터 연산을 함수의 계산으로 보는 사고방식입니다. 사실 함수형 프로그래밍에 대해 이야기한 다음 클로저에 대해 이야기해야 합니다. 왜냐하면 클로저 자체가 함수이기 때문입니다. 형식 프로그래밍의 특징

함수형 프로그래밍에서 함수는 일급 객체입니다. 즉, 함수는 다른 함수의 입력 매개변수 값으로 사용될 수 있고, 함수에서 값을 반환할 수도 있고, 수정되거나 할당될 수도 있음을 의미합니다. 변수. (위키피디아)

일반적으로 순수 함수형 프로그래밍 언어는 프로그램 상태와 가변 객체의 직접적인 사용을 허용하지 않습니다. 함수형 프로그래밍 자체는 공유 상태, 변수 상태 사용을 피하고 부작용을 피하는 것입니다. 가능해요 .

함수형 프로그래밍에는 일반적으로 다음과 같은 특징이 있습니다.

  • 함수는 일급 시민입니다. 함수는 먼저 배치되고 매개변수로 사용될 수 있고, 값을 할당할 수 있고, 전달될 수 있으며, 반환 값으로 사용될 수 있습니다.

  • 부작용 없음: 함수는 완전히 독립적으로 유지되어야 하며 외부 변수의 값을 수정할 수 없고 외부 상태를 수정하지 않습니다.

  • 참조 투명성: 함수 연산은 외부 변수나 상태에 의존하지 않습니다. 동일한 입력 매개변수를 사용하면 어떤 경우에도 반환 값이 동일해야 합니다.

1.2.2 함수 범위

Scope(범위), 프로그래밍 개념 일반적으로 프로그램 코드에 사용되는 이름은 항상 유효/사용 가능한 것은 아니며 이를 제한합니다. 이름의 가용성은 이름의 범위입니다.

일반인의 용어로 함수 범위는 함수가 작동할 수 있는 범위를 나타냅니다. 함수는 상자와 비슷합니다. 범위는 닫힌 상자, 즉 상자 내부에서만 사용할 수 있고 독립적인 범위가 되는 함수의 지역 변수로 이해할 수 있습니다.

Golang의 클로저에 대한 간략한 분석

함수 내 지역 변수가 함수를 떠난 후 범위 밖으로 튀어나와 변수를 찾을 수 없습니다. (외부 함수의 범위에는 내부 함수가 포함되므로 내부 함수는 외부 함수의 지역 변수를 사용할 수 있습니다.) 예를 들어 다음 innerTmep 出了函数作用域就找不到该变量,但是 outerTemp은 내부 함수에서도 사용할 수 있습니다.

Golang의 클로저에 대한 간략한 분석

어떤 언어이든 기본적으로 사용되지 않는 메모리 공간을 재활용하는 특정 메모리 재활용 메커니즘이 있습니다. 재활용 메커니즘은 일반적으로 위에서 언급한 기능의 범위와 관련이 있으며 로컬 변수가 그 역할을 합니다. . 도메인이 재활용될 수 있습니다. 계속 참조되는 경우 재활용되지 않습니다.

1.2.3 범위 상속 관계

소위 범위 상속이란 앞서 언급한 작은 상자가 외부 큰 상자의 범위를 상속받을 수 있다는 의미입니다. 작은 상자에서는 큰 상자의 것들을 직접 가져갈 수 있습니다. 하지만 큰 상자 안에 있는 물건은 탈출이 발생하지 않는 한 꺼낼 수 없습니다. (탈출은 작은 상자에 있는 물건이 참고 사항을 제공하는 것으로 이해될 수 있으며, 큰 상자는 받자마자 사용할 수 있습니다. ). 일반적으로 변수 범위에는 두 가지 유형이 있습니다.

  • 전역 범위: 어디서나 작동

  • 로컬 범위: 일반적으로 코드 블록, 함수, 패키지, 내부 함수선언/정의 변수를 로컬 변수라고 합니다. , 그리고 범위는 함수 내부로 제한됩니다

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입니다. 실제로 이 상황을 요약하면 다음과 같습니다.
  • 함수 안에 또 다른 함수가 중첩되어 있습니다. body이며 반환 값은 function 입니다.
  • 내부 함수는 외부 함수 이외의 위치에서 노출되고 참조되어 클로저를 형성합니다.

이때 궁금한 분들이 계시겠지만, 이전에는 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
로그인 후 복사

从上面结果可以看出,闭包是函数返回函数的时候,不管多少个返回值(函数),都是一次闭包,如果返回的函数有使用外部函数变量,则会绑定到一起,相互影响:

Golang의 클로저에 대한 간략한 분석

闭包绑定了周围的状态,我理解此时的函数就拥有了状态,让函数具有了对象所有的能力,函数具有了状态。

1.3.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
로그인 후 복사

1.3.2 闭包延迟化

上面的例子仿佛都在告诉我们,闭包创建的时候,数据就已经拷贝了,但是真的是这样么?

下面是继续前面的实验:

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
로그인 후 복사
로그인 후 복사
🎜 위 결과에서 알 수 있듯이 클로저는 함수 반환입니다. 함수를 사용할 때 반환 값(함수)이 아무리 많아도 반환된 함수가 외부 함수 변수를 사용하는 경우 모두 클로저입니다. 함께 묶여 서로 영향을 미칩니다: 🎜🎜Golang의 클로저에 대한 간략한 분석🎜🎜클로저가 주변 상태를 묶는다고 이해가 되네요. 당시의 함수에는 상태가 있어서 함수에는 객체의 모든 기능이 있고 함수에는 상태가 있습니다. 🎜

🎜1.3.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에는 참조 전달이 없고 값 전달만 있습니다. 포인터를 전달할 때 값으로도 전달됩니다. 값). 🎜🎜우리 함수의 매개변수가 포인터인 경우 매개변수는 포인터 주소를 복사하여 매개변수로 전달합니다. 본질은 여전히 ​​주소이기 때문에 내부적으로 수정되면 외부에 영향을 미칠 수 있습니다. 🎜🎜클로저의 데이터는 실제로 동일한 주소를 가지고 있음을 증명할 수 있습니다. 🎜
 go build --gcflags=-m main.go
로그인 후 복사
로그인 후 복사
🎜출력은 다음과 같습니다. 따라서 클로저가 외부 환경의 포인터 데이터를 참조하는 경우에는 다음과 같이 추론할 수 있습니다. 포인터 주소 데이터만 복사할 것이며 실제 데이터의 복사본이 아닙니다. (==질문을 먼저 남겨주세요: 복사 시점은 언제입니까==): 🎜
go tool compile -N -l -S main.go
로그인 후 복사
로그인 후 복사

🎜1.3.2 Closure deferral 🎜🎜 🎜위 예시를 보면 클로저가 생성되면 데이터가 복사된 것처럼 보이지만, 과연 그럴까요? 🎜🎜다음은 이전 실험의 연속입니다. 🎜
"".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
로그인 후 복사
🎜클로저를 생성한 후 데이터를 변경한 다음 클로저를 실행했습니다. 대답은 모두 포인터이고 클로저이기 때문에 클로저 실행에 실제로 영향을 미친다는 것입니다. 동일한 복사본을 가리킵니다. 데이터: 🎜rrreee🎜 클로저 함수를 선언한 후 작성 방법을 변경하고 클로저 외부 환경의 변수를 수정한다고 가정해 보겠습니다. 🎜rrreee🎜실제 실행 결과, 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

总结一下上面的验证点:

  • 闭包每次返回都是一个新的实例,每个实例都有一份自己的环境。
  • 同一个实例多次执行,会使用相同的环境。
  • 闭包如果逃逸的是指针,会相互影响,因为绑定的是指针,相同指针的内容修改会相互影响。
  • 闭包并不是在声明时绑定的值,声明后只是预留了外部环境(逃逸分析),真正执行闭包函数时,会获取最新的外部环境的值(也称为延迟绑定)。
  • Go Routine的匿名函数的延迟绑定本质上就是闭包的延迟绑定。

2、闭包的好处与坏处?

2.1 好处

纯函数没有状态,而闭包则是让函数轻松拥有了状态。但是凡事都有两面性,一旦拥有状态,多次调用,可能会出现不一样的结果,就像是前面测试的 case 中一样。那么问题来了:

Q:如果不支持闭包的话,我们想要函数拥有状态,需要怎么做呢?

A: 需要使用全局变量,让所有函数共享同一份变量。

但是我们都知道全局变量有以下的一些特点(在不同的场景,优点会变成缺点):

  • 常驻于内存之中,只要程序不停会一直在内存中。
  • 污染全局,大家都可以访问,共享的同时不知道谁会改这个变量。

闭包可以一定程度优化这个问题:

  • 不需要使用全局变量,外部函数局部变量在闭包的时候会创建一份,生命周期与函数生命周期一致,闭包函数不再被引用的时候,就可以回收了。
  • 闭包暴露的局部变量,外界无法直接访问,只能通过函数操作,可以避免滥用。

除了以上的好处,像在 JavaScript 中,没有原生支持私有方法,可以靠闭包来模拟私有方法,因为闭包都有自己的词法环境。

2.2 坏处

函数拥有状态,如果处理不当,会导致闭包中的变量被误改,但这是编码者应该考虑的问题,是预期中的场景。

闭包中如果随意创建,引用被持有,则无法销毁,同时闭包内的局部变量也无法销毁,过度使用闭包会占有更多的内存,导致性能下降。一般而言,能共享一份闭包(共享闭包局部变量数据),不需要多次创建闭包函数,是比较优雅的方式。

3、闭包怎么实现的?

从上面的实验中,我们可以知道,闭包实际上就是外部环境的逃逸,跟随着闭包函数一起暴露出去。

我们用以下的程序进行分析:

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
로그인 후 복사
로그인 후 복사

Golang의 클로저에 대한 간략한 분석

可以看到 变量 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创建出来的:

Golang의 클로저에 대한 간략한 분석

힙에서 i 사용: i

Golang의 클로저에 대한 간략한 분석

也就是返回函数的时候,实际上返回结构体,结构体里面记录了函数的引用环境。

4、浅聊一下

4.1 Java 支不支持闭包?

网上有很多种看法,实际上 Java 虽然暂时不支持返回函数作为返参,但是Java 本质上还是实现了闭包的概念的,所使用的的方式是内部类的形式,因为是内部类,所以相当于自带了一个引用环境,算是一种不完整的闭包。

目前有一定限制,比如是 final

Golang의 클로저에 대한 간략한 분석

즉, 함수를 반환할 때 실제로 함수의 참조 환경을 기록한 구조체를 반환합니다.

4. 간단히 얘기해보자

Golang의 클로저에 대한 간략한 분석

4.1 Java는 클로저를 지원하나요? 사실 Java는 현재 반환 함수를 반환 매개변수로 지원하지 않지만 Java는 기본적으로 클로저 개념을 구현하고 사용하는 메소드를 내부 클래스 형태로 사용하기 때문에 많은 의견이 있습니다. 내부 클래스이므로 불완전한 폐쇄로 간주되는 참조 환경을 가져오는 것과 같습니다.

현재 특정 제한 사항이 있습니다. 예를 들어 final 로 선언되거나 명확하게 정의된 값이 전달될 수 있는 경우에만 가능합니다.

Stack Overflow에 관련 답변이 있습니다:

stackoverflow.com/questions /5…

4.2 함수형 프로그래밍의 미래는 무엇입니까?

🎜Wiki의 내용은 다음과 같습니다. 🎜🎜🎜함수형 프로그래밍은 오랫동안 학계에서 인기가 있었지만 산업용 애플리케이션은 거의 없습니다. 이러한 상황이 발생하는 가장 큰 이유는 함수형 프로그래밍이 CPU 및 메모리 리소스를 심각하게 소모한다고 간주되는 경우가 많기 때문입니다[🎜18]🎜 이는 함수형 프로그래밍 언어의 초기 구현에서 효율성 문제를 고려하지 않았으며, 함수형 프로그래밍의 특성 때문입니다. 예를 들어 🎜참조 투명성🎜 등을 보장하려면 고유한 데이터 구조와 알고리즘이 필요합니다. [🎜19]🎜🎜🎜그러나 최근에는 상업용 또는 산업용 시스템에서 여러 기능적 프로그래밍 언어가 사용되었습니다 [🎜20]🎜. 예: 🎜
  • Erlang은 스웨덴 회사인 Ericsson이 1980년대 후반에 개발했으며 원래는 내결함성 통신 시스템을 구현하는 데 사용되었습니다. 이후 Nortel, Facebook, Électricité de FranceWhatsApp과 같은 회사에서 일련의 응용 프로그램을 만드는 데 인기 있는 언어로 사용되었습니다. [21][22]
  • Scheme은 초기 Apple Macintosh 컴퓨터에서 여러 응용 프로그램의 기반으로 사용되었으며 최근에는 훈련 시뮬레이션 소프트웨어 및 망원경 제어 방향 등에 적용되고 있습니다. .
  • OCaml은 1990년대 중반에 출시되어 재무 분석, 운전자 검증, 산업용 로봇 프로그래밍, 임베디드 소프트웨어의 정적 분석 등의 분야에서 상용 응용 프로그램을 찾았습니다.
  • Haskell은 원래 연구용 언어로 사용되었지만 항공우주 시스템, 하드웨어 설계, 네트워크 프로그래밍과 같은 분야의 여러 회사에서도 사용되었습니다.산업에서 사용되는 다른 기능적 프로그래밍 언어에는 다중 파라디가
  • 스칼라
[

23] , f#, wolfram language , common lisp, standard ml clojure가 포함됩니다. 잠깐. 개인적인 관점에서는 순수 함수형 프로그래밍에 대해 낙관적이지는 않지만, 앞으로는 거의 모든 고급 프로그래밍에 필요한 것이 함수형 프로그래밍에 대한 아이디어를 갖게 될 것이라고 믿습니다. 특히 Java 수용을 기대합니다. 함수형 프로그래밍. 내가 아는 언어 중에서 Go와 JavaScript의 함수형 프로그래밍 기능은 개발자들에게 깊은 사랑을 받고 있습니다(물론 버그를 작성하면 싫어할 것입니다).

요즘 갑자기 인기를 얻는 이유도 세상이 계속 발전하고 메모리가 점점 커지고 있기 때문입니다. 이 요소의 한계가 거의 해방되었습니다.

세상은 다채롭다고 생각합니다. 한 가지가 세상을 지배하는 것은 절대 불가능합니다. 프로그래밍 언어나 프로그래밍 패러다임에도 마찬가지입니다. 미래에는 결국 역사는 인류 사회의 발전에 맞는 것들을 걸러낼 것입니다.

더 많은 프로그래밍 관련 지식을 보려면

프로그래밍 비디오

를 방문하세요! !

위 내용은 Golang의 클로저에 대한 간략한 분석의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

관련 라벨:
원천:juejin.cn
본 웹사이트의 성명
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.
최신 이슈
인기 튜토리얼
더>
최신 다운로드
더>
웹 효과
웹사이트 소스 코드
웹사이트 자료
프론트엔드 템플릿