Heim  >  Artikel  >  Backend-Entwicklung  >  Eine kurze Analyse der Schließungen in Golang

Eine kurze Analyse der Schließungen in Golang

青灯夜游
青灯夜游nach vorne
2022-11-21 20:36:036510Durchsuche

Eine kurze Analyse der Schließungen in Golang

1. Was ist Schließung?

Bevor wir tatsächlich über Schließungen sprechen, legen wir einige Grundlagen fest:

  • Funktionale Programmierung
  • Funktionsbereich
  • Vererbungsbeziehung des Bereichs

[Verwandte Empfehlungen: Go-Video-Tutorial]

. 1.1 Vorkenntnisse erforderlich

1.2.1 Funktionale Programmierung

Funktionale Programmierung ist ein Programmierparadigma, eine Art, Probleme zu betrachten. Jede Funktion ist so konzipiert, dass sie mithilfe kleiner Funktionen in größere Funktionen organisiert werden kann. Die Parameter von Funktionen sind ebenfalls Funktionen. und die von Funktionen zurückgegebenen Funktionen sind auch Funktionen. Unsere gemeinsamen Programmierparadigmen sind:

  • Imperative Programmierung:
    • Die Hauptidee ist: Achten Sie auf die vom Computer ausgeführten Schritte, dh sagen Sie dem Computer Schritt für Schritt, was er zuerst und dann tun soll.
    • Standardisieren Sie zunächst die Schritte zur Lösung des Problems und abstrahieren Sie es in einen bestimmten Algorithmus. Schreiben Sie dann einen spezifischen Algorithmus, um es zu implementieren. Im Allgemeinen können wir es als prozedurale Programmiersprache bezeichnen, solange die Sprache das prozedurale Programmierparadigma unterstützt. wie BASIC, C usw.
  • Deklarative Programmierung:
    • Die Hauptidee besteht darin, dem Computer zu sagen, was er tun soll, aber nicht anzugeben, wie es zu tun ist, wie z. B. SQL, HTML und CSS für die Webprogrammierung.
  • Funktionale Programmierung:
    • Konzentriert sich nur darauf, was zu tun ist, und nicht darauf, wie es zu tun ist. Es gibt einen Hauch von deklarativer Programmierung, aber es konzentriert sich mehr auf das Prinzip „Funktion steht an erster Stelle“, das heißt, Funktionen können auftreten überall Orte, Parameter, Variablen, Rückgabewerte usw.

Funktionale Programmierung kann als das Gegenteil von objektorientierter Programmierung angesehen werden. Im Allgemeinen legen nur einige Programmiersprachen Wert auf eine bestimmte Programmiermethode. Die meisten Sprachen sind Multiparadigmensprachen und können mehrere unterstützen verschiedene Programmiermethoden wie JavaScript, Go usw.

Funktionale Programmierung ist eine Denkweise, die Computeroperationen als die Berechnung von Funktionen betrachtet. Tatsächlich sollte ich über funktionale Programmierung und dann über Abschlüsse sprechen, da Abschlüsse selbst Funktionen sind die Merkmale der formalen Programmierung.

In der funktionalen Programmierung ist eine Funktion ein erstklassiges Objekt, was bedeutet, dass eine Funktion als Eingabeparameterwert für andere Funktionen verwendet werden kann, auch einen Wert von der Funktion zurückgeben, geändert oder zugewiesen werden kann eine Variable. (Wikipedia)

Im Allgemeinen erlauben reine funktionale Programmiersprachen keine direkte Verwendung des Programmstatus und veränderlicher Objekte. Die Verwendung von gemeinsamen Zuständen und Variablenzuständen sowie Nebenwirkungen sind nicht möglich möglich .

Funktionale Programmierung weist im Allgemeinen die folgenden Merkmale auf:

  • Funktionen sind erstklassige Bürger: Funktionen stehen an erster Stelle und können als Parameter verwendet werden, ihnen können Werte zugewiesen, übergeben und als Rückgabewerte verwendet werden.

  • Keine Nebenwirkungen: Die Funktion muss völlig unabhängig bleiben, kann den Wert externer Variablen nicht ändern und ändert keine externen Zustände.

  • Referenztransparenz: Die Funktionsoperation ist nicht von externen Variablen oder Zuständen abhängig. Bei gleichen Eingabeparametern sollte der Rückgabewert auf jeden Fall gleich sein.

1.2.2 Funktionsumfang

Scope (Bereich), ein Programmierkonzept, das im Allgemeinen nicht immer gültig/verfügbar ist und den Codeumfang einschränkt Die Verfügbarkeit eines Namens ist der Umfang des Namens.

Laienhaft ausgedrückt bezieht sich der Funktionsumfang auf den Bereich, in dem eine Funktion arbeiten kann. Eine Funktion ist ein bisschen wie eine Box, eine Ebene in einer anderen. Wir können den Bereich als geschlossene Box verstehen, das heißt, die lokalen Variablen der Funktion können nur innerhalb der Box verwendet werden und zu einem unabhängigen Bereich werden.

Eine kurze Analyse der Schließungen in Golang

Die lokale Variable innerhalb der Funktion springt nach dem Verlassen der Funktion aus dem Gültigkeitsbereich und die Variable kann nicht gefunden werden. (Die innere Funktion kann die lokalen Variablen der äußeren Funktion verwenden, da der Umfang der äußeren Funktion die innere Funktion umfasst. Beispielsweise kann das Folgende

weiterhin in der inneren Funktion verwendet werden.) innerTmep 出了函数作用域就找不到该变量,但是 outerTemp

Eine kurze Analyse der Schließungen in Golang

Egal um welche Sprache es sich handelt, es gibt grundsätzlich einen bestimmten Speicherrecyclingmechanismus, der ungenutzten Speicherplatz recycelt. Der Recyclingmechanismus hängt im Allgemeinen mit dem Umfang der oben genannten Funktion zusammen, und lokale Variablen spielen eine Rolle . Die Domäne kann recycelt werden, wenn noch darauf verwiesen wird.

1.2.3 Vererbungsbeziehung des Bereichs

Die sogenannte Bereichsvererbung bedeutet, dass die zuvor erwähnte kleine Box den Umfang der äußeren großen Box erben kann. In der kleinen Box können die Dinge in der großen Box direkt übernommen werden heraus, aber die große Kiste kann nicht herausgenommen werden, es sei denn, es kommt zu einer Flucht (Flucht kann als Referenz für die Dinge in der kleinen Kiste verstanden werden, und die große Kiste kann verwendet werden, sobald Sie sie erhalten ). Im Allgemeinen gibt es zwei Arten von Gültigkeitsbereichen von Variablen:

  • Globaler Gültigkeitsbereich: Wirkt überall

  • Lokaler Gültigkeitsbereich: Im Allgemeinen Codeblöcke, Funktionen, Pakete, Innere FunktionenDeklaration/Definition Die Variablen werden lokale Variablen genannt , und der Bereich ist auf das Innere der Funktion beschränkt

1.2 Definition des Abschlusses

„In den meisten Fällen verstehen wir nicht zuerst und definieren dann, sondern definieren zuerst und verstehen dann“ , lass es uns zuerst definieren, Es spielt keine Rolle, wenn du es nicht verstehst:

Ein Abschluss ist eine Kombination aus einer Funktion und ihrer gebündelten Referenz auf die umgebende Umgebung (lexikalische Umgebung, lexikalische Umgebung) . Mit anderen Worten: Abschlüsse ermöglichen Entwicklern den Zugriff auf den Umfang einer äußeren Funktion von einer inneren Funktion aus. Abschlüsse werden erstellt, wenn die Funktion erstellt wird.

In einem Satz erklärt:

Abschluss = Funktion + Referenzumgebung Abschluss = Funktion + Referenzumgebung

Die Wörter Go-Sprache können in der obigen Definition nicht gefunden werden. Sie müssen wissen, dass Schließungen nicht nur für JavaScript oder Go gelten, sondern nur für funktionale Programmiersprachen richtig, Jede Sprache, die funktionale Programmierung unterstützt, unterstützt Abschlüsse, Go und JavaScript sind zwei davon. Die aktuelle Version von Java unterstützt auch Abschlüsse, aber einige Leute denken vielleicht, dass es sich nicht um einen perfekten Abschluss handelt. Details werden im Text besprochen . 1.3 So schreiben Sie Abschlüsse Code im Inneren Die >sum()-Methode kann auf die Parameter und lokalen Variablen der externen Funktion lazySum() verweisen und die Funktion sum() in <code> zurückgeben lazySum() code> werden die relevanten Parameter und Variablen in der zurückgegebenen Funktion gespeichert und können später aufgerufen werden.

Die obige Funktion geht möglicherweise noch einen Schritt weiter, um die gebündelte Funktion und ihren umgebenden Zustand widerzuspiegeln. Wir fügen eine Anzahl von Malen count hinzu:

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
}
Was gibt der obige Code aus? Wird sich die Häufigkeit von count ändern? count ist offensichtlich eine lokale Variable der äußeren Funktion, aber in der Speicherfunktionsreferenz (Bündelung) wird die innere Funktion offengelegt und ausgeführt Das Ergebnis ist wie folgt:
先获取函数,不求结果
等待一会
求结果...
结果: 15
Das Ergebnis ist count. Tatsächlich ändert es sich jedes Mal: ​​

  • In der Funktion ist eine andere Funktion verschachtelt Körper, und der Rückgabewert ist eine Funktion.
  • Die innere Funktion wird von anderen Stellen als der äußeren Funktion verfügbar gemacht und referenziert, wodurch ein Abschluss entsteht.
Einige Leute haben zu diesem Zeitpunkt möglicherweise Fragen. Die vorherige ist, dass lazySum() einmal erstellt und dreimal ausgeführt wurde, aber wenn es dreimal ausgeführt wird wird anders sein. Wie wird es sein? Experiment: Die Ausführungsergebnisse von

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
}
sind wie folgt, jede Ausführung erfolgt zum ersten Mal: ​​
先获取函数,不求结果
等待一会
第 1 次求结果...
结果: 15
第 2 次求结果...
结果: 15
第 3 次求结果...
结果: 15

Aus den obigen Ausführungsergebnissen ist ersichtlich:

Wenn der Abschluss erstellt wird, zählt die referenzierte externe Variable wurde bereits 1 Kopie erstellt, das heißt, es spielt keine Rolle, ob Sie jede einzelne aufrufen

.

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

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

Eine kurze Analyse der Schließungen in 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 {...}这句话之后,就将外部环境的 countStellen Sie weiterhin eine Frage auf: **Wenn eine Funktion zwei Funktionen zurückgibt, handelt es sich dabei um einen Abschluss oder um zwei Abschlüsse? **Üben wir es unten: Gib zwei Funktionen gleichzeitig zurück, eine dient zur Berechnung des Summenergebnisses und die andere zur Berechnung des Produkts:

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
}
🎜Die laufenden Ergebnisse lauten wie folgt: 🎜
先获取函数,不求结果
等待一会
第 1 次求结果...
结果: 15
go func 修改后的变量 count: 101
第 102 次求结果...
结果: 15
🎜 Wie aus den obigen Ergebnissen ersichtlich ist, handelt es sich beim Abschluss um eine Funktionsrückgabe. Unabhängig davon, wie viele Rückgabewerte (Funktionen) vorhanden sind, handelt es sich bei allen um einen Abschluss. Wenn die zurückgegebene Funktion externe Funktionsvariablen verwendet, ist dies der Fall miteinander verbunden sein und sich gegenseitig beeinflussen: 🎜🎜Eine kurze Analyse der Schließungen in Golang🎜🎜Der Abschluss bindet den umgebenden Zustand, ich verstehe das. Die Funktion hat zu diesem Zeitpunkt einen Zustand, sodass die Funktion alle Fähigkeiten des Objekts hat und die Funktion einen Zustand hat. 🎜

🎜1.3.2 Zeiger und Werte in Abschlüssen🎜🎜🎜Im obigen Beispiel sind alle in unseren Abschlüssen verwendeten Werte numerische Werte. Was passiert, wenn wir bestehen Hinweise? Was für ein Ding? 🎜
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())
}
🎜Die laufenden Ergebnisse sind wie folgt: 🎜
5
🎜Es ist ersichtlich, dass, wenn es sich um einen Zeiger handelt und der Wert der dem Zeiger entsprechenden Adresse im Abschluss geändert wird, dies auch Auswirkungen auf den Wert außerhalb des Abschlusses hat. Das ist eigentlich leicht zu verstehen. In Go gibt es keine Referenzübergabe, sondern nur eine Wertübergabe. Der Wert wird hier auch als Wert des Zeigers übergeben ). 🎜🎜Wenn der Parameter unserer Funktion ein Zeiger ist, kopiert der Parameter die Zeigeradresse und übergibt sie als Parameter. Da die Essenz immer noch eine Adresse ist, kann sie bei interner Änderung immer noch Auswirkungen auf die Außenseite haben. 🎜🎜Die Daten im Abschluss haben tatsächlich die gleiche Adresse: 🎜
 go build --gcflags=-m main.go
🎜Die Ausgabe lautet daher wie folgt: Wenn sich der Abschluss auf die Zeigerdaten der externen Umgebung bezieht, ist dies der Fall kopiert nur die Zeiger-Adressdaten und ist keine Kopie der echten Daten (==Lassen Sie zuerst eine Frage: Wann ist der Zeitpunkt des Kopierens==): 🎜
go tool compile -N -l -S main.go

🎜1.3.2 Schließungsaufschub 🎜🎜 🎜Die obigen Beispiele scheinen uns zu sagen, dass die Daten beim Erstellen der Schließung kopiert wurden, aber ist das wirklich der Fall? 🎜🎜Das Folgende ist die Fortsetzung des vorherigen Experiments: 🎜
"".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
🎜Nachdem wir den Abschluss erstellt haben, ändern wir die Daten und führen dann den Abschluss aus. Die Antwort muss sein, dass es sich wirklich auf die Ausführung des Abschlusses auswirkt, da es sich bei allen um Zeiger handelt und zeigen Sie auf dieselbe Kopie wird der geänderte Wert sein: 🎜rrreee🎜Dies beweist auch, dass der Abschluss den der externen Umgebung nach der Deklaration von <code>var sum = func() int {...} tatsächlich nicht ändert /code> count ist an den Abschluss gebunden, aber es ist gebunden, wenn die Funktion die Abschlussfunktion zurückgibt. Dies ist eine 🎜verzögerte Bindung🎜. 🎜

如果还没看明白没关系,我们再来一个例子:

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

Eine kurze Analyse der Schließungen in 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创建出来的:

Eine kurze Analyse der Schließungen in Golang

Verwenden Sie den i auf dem Heap: i

Eine kurze Analyse der Schließungen in Golang

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

4、浅聊一下

4.1 Java 支不支持闭包?

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

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

Eine kurze Analyse der Schließungen in Golang

Das heißt, wenn eine Funktion zurückgegeben wird, gibt sie tatsächlich eine Struktur zurück, die die Referenzumgebung der Funktion aufzeichnet.

4. Lass uns kurz reden

Eine kurze Analyse der Schließungen in Golang

4.1 Unterstützt Java Schließungen? Im Internet gibt es viele Meinungen. Obwohl Java derzeit keine Rückgabefunktionen als Rückgabeparameter unterstützt, implementiert Java im Wesentlichen das Konzept des Abschlusses und die verwendete Methode liegt in der Form einer inneren Klasse vor ist eine interne Klasse und entspricht daher dem Einbringen einer Referenzumgebung, was als unvollständiger Abschluss angesehen wird.

Es gibt derzeit bestimmte Einschränkungen, zum Beispiel nur, wenn es mit final deklariert wird oder ein klar definierter Wert übergeben werden kann:

Es gibt entsprechende Antworten auf Stack Overflow:

stackoverflow.com/questions /5…

4.2 Wie sieht die Zukunft der funktionalen Programmierung aus?

🎜Hier ist, was das Wiki sagt: 🎜🎜🎜Funktionale Programmierung ist in der Wissenschaft seit langem beliebt, hat aber nur wenige industrielle Anwendungen. Der Hauptgrund für diese Situation ist, dass funktionale Programmierung oft als stark verbrauchend auf CPU- und Speicherressourcen angesehen wird [🎜18]🎜 Dies liegt daran, dass Effizienzaspekte bei der frühen Implementierung funktionaler Programmiersprachen nicht berücksichtigt wurden, und aufgrund der Eigenschaften funktionaler Programmiersprachen Programmierung, um beispielsweise „Referenztransparenz“ usw. sicherzustellen, erfordert einzigartige Datenstrukturen und Algorithmen. [🎜19]🎜🎜🎜Allerdings werden in letzter Zeit mehrere funktionale Programmiersprachen in kommerziellen oder industriellen Systemen verwendet [🎜20]🎜, zum Beispiel: 🎜
  • Erlang, das Ende der 1980er Jahre von der schwedischen Firma Ericsson entwickelt wurde, diente ursprünglich der Implementierung fehlertoleranter Telekommunikationssysteme. Seitdem wird es als beliebte Sprache zum Erstellen einer Reihe von Anwendungen von Unternehmen wie Nortel, Facebook, Électricité de France und WhatsApp verwendet. [21][22]
  • Schema, das als Grundlage für mehrere Anwendungen auf frühen Apple MacintoshComputern diente und kürzlich auf Dinge wie Trainingssimulationssoftware und Teleskopsteuerungsrichtung angewendet wurde .
  • OCaml, das Mitte der 1990er Jahre auf den Markt kam, hat kommerzielle Anwendungen in Bereichen wie Finanzanalyse, Fahrerverifizierung, Industrieroboterprogrammierung und statischer Analyse eingebetteter Software gefunden.
  • Haskell, obwohl es ursprünglich als Forschungssprache verwendet wurde, wurde es auch von einer Reihe von Unternehmen in Bereichen wie Luft- und Raumfahrtsystemen, Hardwaredesign und Netzwerkprogrammierung verwendet.

Weitere funktionale Programmiersprachen, die in der Industrie verwendet werden, sind das Multiparadigma Scala[23], F# sowie Wolfram Language, Common Lisp, Standard ML und Clojure Warte.

Aus meiner persönlichen Sicht bin ich nicht optimistisch, was die reine funktionale Programmierung angeht, aber ich glaube, dass fast jeder fortgeschrittene Programmierbedarf in Zukunft die Idee der funktionalen Programmierung haben wird. Ich freue mich besonders darauf, Java anzunehmen funktionale Programmierung. Von den Sprachen, die ich kenne, sind die funktionalen Programmierfunktionen in Go und JavaScript bei Entwicklern sehr beliebt (natürlich werden Sie sie hassen, wenn Sie Fehler schreiben).

Der Grund, warum es in letzter Zeit plötzlich populär geworden ist, liegt auch darin, dass sich die Welt weiterentwickelt und das Gedächtnis immer größer wird. Die Einschränkungen dieses Faktors werden nahezu aufgehoben.

Ich glaube, dass es absolut unmöglich ist, dass eine Sache die Welt beherrscht. Das Gleiche gilt für Programmiersprachen oder Programmierparadigmen In der Zukunft wird die Geschichte diejenigen herausfiltern, die der Entwicklung der menschlichen Gesellschaft entsprechen.

Weitere Kenntnisse zum Thema Programmierung finden Sie unter: Programmiervideos! !

Das obige ist der detaillierte Inhalt vonEine kurze Analyse der Schließungen in Golang. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!

Stellungnahme:
Dieser Artikel ist reproduziert unter:juejin.cn. Bei Verstößen wenden Sie sich bitte an admin@php.cn löschen