Diese Serie enthält vier Artikel, in denen hauptsächlich die Mechanismen und Designkonzepte hinter Go-Sprachzeigern, Stapeln, Heaps, Escape-Analyse und Wert-/Zeigersemantik erläutert werden. Dies ist der erste Artikel der Reihe, in dem hauptsächlich Stapel und Zeiger erläutert werden.
Ich werde nichts Gutes über Zeiger sagen, es ist wirklich schwer zu verstehen. Bei falscher Anwendung kann es zu lästigen Fehlern und sogar Leistungsproblemen kommen. Dies gilt insbesondere beim Schreiben gleichzeitiger oder Multithread-Software. Es ist kein Wunder, dass viele Programmiersprachen versuchen, die Verwendung von Zeigern für Programmierer zu vermeiden. Wenn Sie jedoch die Programmiersprache Go verwenden, sind Zeiger unvermeidlich. Nur wenn Sie die Hinweise genau verstehen, können Sie sauberen, prägnanten und effizienten Code schreiben.
Frame-Grenze bietet einen separaten Speicherplatz für jede Funktion, und die Funktion wird innerhalb der Frame-Grenze ausgeführt. Frame-Grenzen ermöglichen die Ausführung von Funktionen in ihrem eigenen Kontext und bieten außerdem eine Flusskontrolle. Funktionen können über den Frame-Zeiger direkt auf den Speicher innerhalb des Frames zugreifen, während der Zugriff auf Speicher außerhalb des Frames nur indirekt erfolgen kann. Wenn Sie für jede Funktion auf Speicher außerhalb des Rahmens zugreifen möchten, muss dieser Speicher mit der Funktion geteilt werden. Um die gemeinsame Implementierung zu verstehen, müssen wir zunächst die Mechanismen und Einschränkungen zum Festlegen von Rahmengrenzen kennen und verstehen.
Beim Aufruf einer Funktion findet ein Kontextwechsel zwischen zwei Framegrenzen statt. Von der aufrufenden Funktion zur aufgerufenen Funktion: Wenn beim Aufruf der Funktion Parameter übergeben werden müssen, müssen diese Parameter auch innerhalb der Rahmengrenzen der aufgerufenen Funktion übergeben werden. In der Go-Sprache werden Daten zwischen zwei Frames nach Wert übertragen.
Der Vorteil der Datenübergabe nach Wert ist die gute Lesbarkeit. Wenn eine Funktion aufgerufen wird, ist der angezeigte Wert der Wert, der zwischen dem Funktionsaufrufer und dem Aufgerufenen kopiert und empfangen wurde. Deshalb verbinde ich „Wertübergabe“ mit WYSIWYG, denn was man sieht, ist das, was man bekommt.
Schauen wir uns einen Code an, der ganzzahlige Daten nach Wert übergibt:
Listing 1
package main func main() { // Declare variable of type int with a value of 10. count := 10 // Display the "value of" and "address of" count. println("count:\tValue Of[", count, "]\tAddr Of[", &count, "]") // Pass the "value of" the count. increment(count) println("count:\tValue Of[", count, "]\tAddr Of[", &count, "]") } //go:noinline func increment(inc int) { // Increment the "value of" inc. inc++ println("inc:\tValue Of[", inc, "]\tAddr Of[", &inc, "]") }
Wenn Sie ein Go-Programm starten, erstellt die Laufzeit eine Haupt-Coroutine, um den gesamten Initialisierungscode einschließlich des Codes in der Funktion main() auszuführen. Goroutine ist ein Ausführungspfad, der im Betriebssystem-Thread platziert wird und letztendlich auf einem bestimmten Kern ausgeführt wird. Ab Go 1.8 weist jede Goroutine einen zusammenhängenden 2048-Byte-Speicherblock als Stapelspeicher zu. Die Größe des anfänglichen Stapelplatzes hat sich im Laufe der Jahre verändert und kann sich in Zukunft noch einmal ändern.
Der Stapel ist sehr wichtig, da er den physischen Speicherplatz für die Rahmengrenzen jeder einzelnen Funktion bereitstellt. Gemäß Listing 1 wird der Stapelspeicher wie folgt verteilt, wenn die Haupt-Coroutine die Funktion main() ausführt:
Abbildung 1
In Abbildung 1 können Sie diesen Teil des Stapels der Hauptfunktion sehen wurde gerahmt kam heraus. Dieser Teil wird als „Stapelrahmen“ bezeichnet und stellt die Grenze der Hauptfunktion auf dem Stapel dar. Der Rahmen wird erstellt, wenn die aufgerufene Funktion ausgeführt wird. Sie können auch sehen, dass die Variablenanzahl im Funktionsrahmen main() an der Speicheradresse 0x10429fa4 platziert wird.
Abbildung 1 zeigt auch einen weiteren interessanten Punkt. Der gesamte Stapelspeicher unterhalb des aktiven Rahmens ist nicht verfügbar. Nur der Stapelspeicher oberhalb des aktiven Rahmens ist verfügbar. Die Grenze zwischen verfügbarem Stapelplatz und nicht verfügbarem Stapelplatz muss geklärt werden.
Der Zweck einer Variablen besteht darin, einer bestimmten Speicheradresse einen Namen zuzuweisen, um den Code besser lesbar zu machen und Ihnen bei der Analyse der von Ihnen verarbeiteten Daten zu helfen. Wenn Sie eine Variable haben und deren Wert im Speicher abgelegt werden kann, muss es in der Speicheradresse eine Adresse geben, die diesen Wert speichert. In Zeile 9 des Codes ruft die Funktion main() die integrierte Funktion println() auf, um den Wert und die Adresse der Variablen count anzuzeigen.
Listing 2
println("count:\tValue Of[", count, "]\tAddr Of[", &count, "]")
Es ist nicht überraschend, den &-Operator zu verwenden, um die Adresse des Speicherorts zu erhalten, an dem sich eine Variable befindet. Andere Sprachen verwenden diesen Operator ebenfalls. Wenn Ihr Code auf einem 32-Bit-Computer, z. B. go Playground, ausgeführt wird, sieht die Ausgabe etwa wie folgt aus:
清单3
count: Value Of[ 10 ] Addr Of[ 0x10429fa4 ]
接下来的第 12 行代码,main() 函数调用 increment() 函数。
清单4
increment(count)
调用函数意味着协程需要在栈上构建出一块新的栈帧。但是,事情有点复杂。要想成功地调用函数,在发生上下文切换时,数据需要跨越帧边界传递到新的帧范围内。具体一点来说,函数调用的时候,整型值会被复制和传递。通过第 18 行代码、increment() 函数的声明,你就可以知道。
清单5
func increment(inc int) {
如果你回过头来再次看第 12 行代码函数 increment() 的调用,你会发现 count 变量是传值的。这个值会被拷贝、传递,最后存储在 increment() 函数的栈中。记住,increment() 函数只能在自己的栈内读写内存,因此,它需要 inc 变量来接收、存储和访问传递的 count 变量的副本。
就在 increment() 函数内部代码开始执行之前,协程的栈(站在一个非常高的角度)应该是像下图这样的:
图 2
你可以看到栈上现在有两个帧,一个属于 main() 函数,另一个属于 increment() 函数。在 increment() 函数的帧里面,你可以看到 inc 变量,它的值 10,是函数调用时拷贝、传递进来的。变量 inc 的地址是 0x10429f98,因为栈帧是从上至下使用栈空间的,所以它的内存地址较小,这只是具体的实现细节,并没任何意义。重要的是,协程从 main() 的栈帧里获取变量 count 的值,并使用 inc 变量将该值的副本放置在 increment() 函数的栈帧里。
increment() 函数的剩余代码显示 inc 变量的值和地址。
清单6
inc++ println("inc:\tValue Of[", inc, "]\tAddr Of[", &inc, "]")
第 22 行代码输出类似下面这样:
清单7
inc: Value Of[ 11 ] Addr Of[ 0x10429f98 ]
执行这些代码之后,栈就会像下面这样:
图 3
第 21、22 行代码执行之后,increment() 函数返回并且 CPU 控制权交还给 main() 函数。第 14 行代码,main() 函数会再次显示 count 变量的值和地址。
清单8
println("count:\tValue Of[",count, "]\tAddr Of[", &count, "]")
上面例子完整的输出会像下面这样:
count: Value Of[ 10 ] Addr Of[ 0x10429fa4 ] inc: Value Of[ 11 ] Addr Of[ 0x10429f98 ] count: Value Of[ 10 ] Addr Of[ 0x10429fa4 ]
main() 函数栈帧里,变量 count 的值在调用 increment() 函数前后是相同的。
当函数返回并且控制权交还给调用函数时,栈上的内存实际上会发生什么?回答是:不会发生任何事情。当 increment() 函数返回时,栈上的空间看起来像下面这样:
Abbildung 4
Abgesehen davon, dass der für die Funktion increment() erstellte Stapelrahmen nicht mehr verfügbar ist, ist die Verteilung des Stapels im Wesentlichen dieselbe wie in Abbildung 3. Dies liegt daran, dass der Frame der Funktion main() zum aktiven Frame wird. Im Stapelrahmen der Funktion increment() findet keine Verarbeitung statt.
Wenn die Funktion zurückkehrt, wird das Bereinigen des Funktionsrahmens Zeit verschwenden, da Sie nicht wissen, ob dieser Speicher erneut verwendet wird. Dieser Speicher führt also keine Verarbeitung durch. Bei jedem Aufruf einer Funktion werden die auf dem Stapel zugewiesenen Frames gelöscht, wenn ein Frame benötigt wird. Dies geschieht beim Initialisieren der im Frame gespeicherten Variablen. Da alle Werte auf ihre entsprechenden Nullwerte initialisiert werden, bereinigt sich der Stapel bei jedem Aufruf der Funktion korrekt.
Was ist, wenn es sehr wichtig ist, dass die Funktion increment() die im Funktionsrahmen main() gespeicherte Zählvariable direkt bedient? Dies erfordert die Verwendung von Zeigern! Der Zweck von Zeigern besteht darin, Werte zwischen Funktionen auszutauschen. Auch wenn der Wert nicht im Rahmen seiner eigenen Funktion liegt, kann die Funktion ihn lesen und schreiben.
Wenn Sie das Konzept des Teilens nicht im Kopf haben, werden Sie wahrscheinlich keine Hinweise verwenden. Beim Erlernen von Zeigern ist es wichtig, ein klares Vokabular zu verwenden, anstatt sich einfach nur Operatoren oder Syntax zu merken. Denken Sie also daran, dass Zeiger dazu gedacht sind, gemeinsam genutzt zu werden. Wenn Sie beim Lesen von Code an „Teilen“ denken, sollten Sie an den &-Operator denken.
Ob er von Ihnen angepasst wird oder mit der Go-Sprache geliefert wird, für jeden deklarierten Typ kann der entsprechende Zeigertyp basierend auf diesen Typen zur gemeinsamen Nutzung abgerufen werden. Beispielsweise ist der integrierte Typ int der entsprechende Zeigertyp *int. Wenn Sie den Typ User selbst deklarieren, ist der entsprechende Zeigertyp *User.
Alle Zeigertypen haben die gleichen Eigenschaften. Erstens beginnen sie mit dem *-Symbol; zweitens belegen sie denselben Speicherplatz und stellen beide eine Adresse dar, wobei sie eine Länge von 4 oder 8 Bytes verwenden, um eine Adresse darzustellen. Auf einem 32-Bit-Computer (z. B. einem Spielplatz) benötigt ein Zeiger 4 Byte Speicher, auf einem 64-Bit-Computer (z. B. Ihrem Computer) 8 Byte Speicher.
规范里有说明,指针类型可以看成是类型字面量,这意味着它们是有现有类型组成的未命名类型。
让我们来看一段代码,这段代码展示了函数调用时按值传递地址。main() 和 increment() 函数的栈帧会共享 count 变量:
清单10
package main func main() { // Declare variable of type int with a value of 10. count := 10 // Display the "value of" and "address of" count. println("count:\tValue Of[", count, "]\t\tAddr Of[", &count, "]") // Pass the "address of" count. increment(&count) println("count:\tValue Of[", count, "]\t\tAddr Of[", &count, "]") } //go:noinline func increment(inc *int) { // Increment the "value of" count that the "pointer points to". (dereferencing) *inc++ println("inc:\tValue Of[", inc, "]\tAddr Of[", &inc, "]\tValue Points To[", *inc, "]") }
基于原来的代码有三处改动的地方,第 12 行是第一处改动:
清单11
increment(&count)
现在,第 12 行代码拷贝、传递的并非 count 变量的值,而是变量的地址。可以认为,main() 函数与 increment() 函数是共享 count 变量的。这是 & 操作符起的作用。
重点理解,现在依旧是传值,唯一不同的是现在传递的是地址而不是一个整型数据。地址也是一个值,是函数调用时会跨帧边界发生拷贝和传递的内容。
因为地址会发生拷贝和传递,在 increment() 函数里面需要一个变量接收和存储该地址值。所以在第 18 行声明了整型的指针变量。
清单12
func increment(inc *int) {
如果你传递的是 User 类型值的地址,变量就应该声明成 *User。尽管指针变量存储的是地址,也不能传递任何类型的地址,只能传递与指针类型相一致的地址。关键在于,共享值的原因是因为接收函数能够对值进行读写操作。只有知道值的类型信息才能够进行读写操作。编译器会保证只有与指针类型相一致的值才能够实现函数间共享。
调用 increment() 函数时候,栈空间就像下面这样:
图 5
当一个地址作为值执行按值传递之后,你可以从图 5 看出栈是如何分布的。现在,increment() 函数帧空间里面的指针变量指向 count 变量,该变量在 main() 函数的帧空间里。
通过使用指针变量,increment() 函数可以间接对 count 变量执行读写操作。
清单 13
*inc++
这一次,字符 * 充当操作符,与指针变量搭配使用。使用 * 操作符是“获取指针指向的值”的意思。指针变量允许在帧外对函数帧内的内存进行间接访问。有时候,间接的读写操作也称为解引用。increment() 函数必须有指针变量,才能够对其他函数帧空间执行间接访问。
执行第 21 行代码之后,栈空间分布如图 6 所示。
图 6
程序最后输出:
清单 14
count: Value Of[ 10 ] Addr Of[ 0x10429fa4 ] inc: Value Of[ 0x10429fa4 ] Addr Of[ 0x10429f98 ] Value Points To[ 11 ] count: Value Of[ 11 ] Addr Of[ 0x10429fa4 ]
你可以看到,指针变量 inc 的值和 count 变量的地址是相同的。这将建立起共享关系,允许在帧外执行内存的间接访问。在 increment() 函数里,一旦通过指针执行了写操作,改变也会体现在 main() 函数里。
指针变量并不特别,它们和其他变量一样也是变量,有内存地址和值。正巧的是,无论指针变量指向的值的类型如何,所有的指针变量都有同样的大小和表现形式。唯一困惑的是使用 * 字符充当操作符,用来声明指针类型。如果你能分清指针类型声明和指针操作,你就没有那么困惑了。
这篇文章描述了设计指针背后的目的和 Go 语言中栈和指针的工作机制。这是理解 Go 语言机制、设计哲学的第一步,也对编写一致性且可读性的代码提供一些指导作用。
总结一下,通过这篇文章你能学习到的知识:
1.Frame-Grenze stellt einen separaten Speicherplatz für jede Funktion bereit, und die Funktion wird innerhalb des Frame-Bereichs ausgeführt 2.Wenn die Funktion aufgerufen wird, wechselt der Kontext zwischen den beiden Frames; 3.Der Vorteil der Wertübergabe ist die gute Lesbarkeit. 4.Der Stapel ist wichtig, da er zugänglichen physischen Speicherplatz für die Rahmengrenze jeder Funktion bietet Der aktive Frame ist nicht verfügbar, nur der aktive Frame und der Stapelspeicher darüber sind nützlich. 6.Der Aufruf der Funktion bedeutet, dass die Coroutine jedes Mal einen neuen Stapelrahmen im Stapelspeicher öffnet Eine Funktion wird aufgerufen. Wenn ein Frame verwendet wird, wird der entsprechende Stapelspeicher initialisiert Stapelrahmen, es kann auch gelesen und geschrieben werden; 9.Für jeden Typ, egal ob er selbst definiert oder in der Go-Sprache integriert ist, gibt es einen entsprechenden Zeigertyp; Die Verwendung von Zeigervariablen ermöglicht den indirekten Speicherzugriff außerhalb des Funktionsrahmens. 11.Zeigervariablen sind im Vergleich zu anderen Variablen nichts Besonderes, da es sich auch um Variablen mit Speicheradressen und -werten handelt.
Das obige ist der detaillierte Inhalt vonGo-Sprachmechanismusstapel und Zeiger. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!