„Wenn ein Arbeiter seine Arbeit gut machen will, muss er zuerst seine Werkzeuge schärfen.“ – Konfuzius, „Die Gespräche des Konfuzius. Lu Linggong“
Titelseite > Programmierung > Wie Go-Arrays funktionieren und wie es mit For-Range knifflig wird

Wie Go-Arrays funktionieren und wie es mit For-Range knifflig wird

Veröffentlicht am 22.08.2024
Durchsuche:813

Dies ist ein Auszug aus dem Beitrag; Der vollständige Beitrag ist hier verfügbar: How Go Arrays Work and Get Tricky with For-Range.

Das klassische Golang-Array und -Slice sind ziemlich einfach. Arrays haben eine feste Größe und Slices sind dynamisch. Aber ich muss Ihnen sagen: Go mag oberflächlich betrachtet einfach erscheinen, aber unter der Haube steckt eine Menge dahinter.

Wie immer beginnen wir mit den Grundlagen und gehen dann etwas tiefer. Keine Sorge, Arrays werden ziemlich interessant, wenn man sie aus verschiedenen Blickwinkeln betrachtet.

Wir werden uns im nächsten Teil mit den Slices befassen, ich werde das hier einfügen, sobald es fertig ist.

Was ist ein Array?

Arrays in Go ähneln denen in anderen Programmiersprachen. Sie haben eine feste Größe und speichern Elemente desselben Typs an zusammenhängenden Speicherorten.

Das bedeutet, dass Go schnell auf jedes Element zugreifen kann, da seine Adressen basierend auf der Startadresse des Arrays und dem Index des Elements berechnet werden.

func main() {
    arr := [5]byte{0, 1, 2, 3, 4}
    println("arr", &arr)

    for i := range arr {
        println(i, &arr[i])
    }
}

// Output:
// arr 0x1400005072b
// 0 0x1400005072b
// 1 0x1400005072c
// 2 0x1400005072d
// 3 0x1400005072e
// 4 0x1400005072f

Hier gibt es ein paar Dinge zu beachten:

  • Die Adresse des Arrays arr ist dieselbe wie die Adresse des ersten Elements.
  • Die Adresse jedes Elements ist 1 Byte voneinander entfernt, da unser Elementtyp Byte ist.

How Go Arrays Work and Get Tricky with For-Range

Array [5]Byte{0, 1, 2, 3, 4} im Speicher

Schauen Sie sich das Bild genau an.

Unser Stack wächst von einer höheren zu einer niedrigeren Adresse nach unten, oder? Dieses Bild zeigt genau, wie ein Array im Stapel aussieht, von arr[4] bis arr[0].

Bedeutet das also, dass wir auf jedes Element eines Arrays zugreifen können, indem wir die Adresse des ersten Elements (oder des Arrays) und die Größe des Elements kennen? Versuchen wir es mit einem int-Array und einem unsicheren Paket:

func main() {
    a := [3]int{99, 100, 101}

    p := unsafe.Pointer(&a[0])

    a1 := unsafe.Pointer(uintptr(p)   8)
    a2 := unsafe.Pointer(uintptr(p)   16)

    fmt.Println(*(*int)(p))
    fmt.Println(*(*int)(a1))
    fmt.Println(*(*int)(a2))
}

// Output:
// 99
// 100
// 101

Nun, wir erhalten den Zeiger auf das erste Element und berechnen dann die Zeiger auf die nächsten Elemente, indem wir Vielfache der Größe eines int addieren, was bei einer 64-Bit-Architektur 8 Bytes entspricht. Dann verwenden wir diese Zeiger, um auf sie zuzugreifen und sie wieder in die int-Werte umzuwandeln.

How Go Arrays Work and Get Tricky with For-Range

Array [3]int{99, 100, 101} im Speicher

Das Beispiel ist nur ein Herumspielen mit dem unsicheren Paket, um zu Bildungszwecken direkt auf den Speicher zuzugreifen. Tun Sie dies nicht in der Produktion, ohne die Konsequenzen zu verstehen.

Nun ist ein Array vom Typ T kein Typ für sich, aber ein Array mit einer bestimmten Größe und dem Typ T wird als Typ betrachtet. Folgendes meine ich:

func main() {
    a := [5]byte{}
    b := [4]byte{}

    fmt.Printf("%T\n", a) // [5]uint8
    fmt.Printf("%T\n", b) // [4]uint8

    // cannot use b (variable of type [4]byte) as [5]byte value in assignment
    a = b 
}

Obwohl sowohl a als auch b Byte-Arrays sind, sieht der Go-Compiler sie als völlig unterschiedliche Typen, das %T-Format macht diesen Punkt deutlich.

So sieht es der Go-Compiler intern (src/cmd/compile/internal/types2/array.go):

// An Array represents an array type.
type Array struct {
    len  int64
    elem Type
}

// NewArray returns a new array type for the given element type and length.
// A negative length indicates an unknown length.
func NewArray(elem Type, len int64) *Array { return &Array{len: len, elem: elem} }

Die Länge des Arrays ist im Typ selbst „kodiert“, sodass der Compiler die Länge des Arrays anhand seines Typs kennt. Der Versuch, ein Array einer Größe einem anderen zuzuordnen oder sie zu vergleichen, führt zu einem Fehler aufgrund eines nicht übereinstimmenden Typs.

Array-Literale

Es gibt viele Möglichkeiten, ein Array in Go zu initialisieren, und einige davon werden in echten Projekten möglicherweise selten verwendet:

var arr1 [10]int // [0 0 0 0 0 0 0 0 0 0]

// With value, infer-length
arr2 := [...]int{1, 2, 3, 4, 5} // [1 2 3 4 5]

// With index, infer-length
arr3 := [...]int{11: 3} // [0 0 0 0 0 0 0 0 0 0 0 3]

// Combined index and value
arr4 := [5]int{1, 4: 5} // [1 0 0 0 5]
arr5 := [5]int{2: 3, 4, 4: 5} // [0 0 3 4 5]

Was wir oben tun (mit Ausnahme des ersten), ist das Definieren und Initialisieren ihrer Werte, was als „zusammengesetztes Literal“ bezeichnet wird. Dieser Begriff wird auch für Slices, Maps und Strukturen verwendet.

Nun, hier ist eine interessante Sache: Wenn wir ein Array mit weniger als 4 Elementen erstellen, generiert Go Anweisungen, um die Werte einzeln in das Array einzufügen.

Wenn wir also arr := [3]int{1, 2, 3, 4} machen, passiert tatsächlich Folgendes:

arr := [4]int{}
arr[0] = 1
arr[1] = 2
arr[2] = 3
arr[3] = 4

Diese Strategie wird als lokale Code-Initialisierung bezeichnet. Dies bedeutet, dass der Initialisierungscode im Rahmen einer bestimmten Funktion generiert und ausgeführt wird und nicht Teil des globalen oder statischen Initialisierungscodes ist.

Es wird klarer, wenn Sie unten eine andere Initialisierungsstrategie lesen, bei der die Werte nicht einzeln im Array platziert werden.

"Was ist mit Arrays mit mehr als 4 Elementen?"

Der Compiler erstellt eine statische Darstellung des Arrays in der Binärdatei, was als Strategie der „statischen Initialisierung“ bekannt ist.

Dies bedeutet, dass die Werte der Array-Elemente in einem schreibgeschützten Abschnitt der Binärdatei gespeichert werden. Diese statischen Daten werden zur Kompilierzeit erstellt, sodass die Werte direkt in die Binärdatei eingebettet werden. Wenn Sie neugierig sind, wie [5]int{1,2,3,4,5} in der Go-Assembly aussieht:

main..stmp_1 SRODATA static size=40
    0x0000 01 00 00 00 00 00 00 00 02 00 00 00 00 00 00 00  ................
    0x0010 03 00 00 00 00 00 00 00 04 00 00 00 00 00 00 00  ................
    0x0020 05 00 00 00 00 00 00 00                          ........

Es ist nicht einfach, den Wert des Arrays zu erkennen, wir können daraus dennoch einige wichtige Informationen erhalten.

Unsere Daten werden in stmp_1 gespeichert, bei dem es sich um schreibgeschützte statische Daten mit einer Größe von 40 Byte (8 Byte für jedes Element) handelt, und die Adresse dieser Daten ist in der Binärdatei fest codiert.

Der Compiler generiert Code, um auf diese statischen Daten zu verweisen. Wenn unsere Anwendung ausgeführt wird, kann sie diese vorinitialisierten Daten direkt verwenden, ohne dass zusätzlicher Code zum Einrichten des Arrays erforderlich ist.

const readonly = [5]int{1, 2, 3, 4, 5}

arr := readonly

"Was wäre mit einem Array mit 5 Elementen, aber nur 3 davon initialisiert?"

Gute Frage, dieses Literal [5]int{1,2,3} fällt in die erste Kategorie, in der Go den Wert einzeln in das Array einfügt.

Während wir über die Definition und Initialisierung von Arrays sprechen, sollten wir erwähnen, dass nicht jedes Array auf dem Stapel zugeordnet ist. Wenn es zu groß ist, wird es auf den Heap verschoben.

Aber wie groß ist „zu groß“, fragen Sie sich vielleicht.

Ab Go 1.23 gilt: Wenn die Größe der Variablen, nicht nur des Arrays, einen konstanten Wert MaxStackVarSize überschreitet, der derzeit 10 MB beträgt, wird sie als zu groß für die Stapelzuweisung betrachtet und auf den Heap verschoben.

func main() {
    a := [10 * 1024 * 1024]byte{}
    println(&a)

    b := [10*1024*1024   1]byte{}
    println(&b)
}

In diesem Szenario wird b auf den Heap verschoben, während a dies nicht tut.

Array-Operationen

Die Länge des Arrays ist im Typ selbst kodiert. Auch wenn Arrays keine Cap-Eigenschaft haben, können wir sie dennoch erhalten:

func main() {
    a := [5]int{1, 2, 3}
    println(len(a)) // 5
    println(cap(a)) // 5
}

Die Kapazität entspricht zweifellos der Länge, aber das Wichtigste ist, dass wir das zur Kompilierungszeit wissen, oder?

Also ergibt len(a) für den Compiler keinen Sinn, da es sich nicht um eine Laufzeiteigenschaft handelt. Der Go-Compiler kennt den Wert zur Kompilierungszeit.

...

Dies ist ein Auszug aus dem Beitrag; Der vollständige Beitrag ist hier verfügbar: How Go Arrays Work and Get Tricky with For-Range.

Freigabeerklärung Dieser Artikel ist abgedruckt unter: https://dev.to/func25/how-go-arrays-work-and-get-tricky-with-for-range-3i9i?1 Bei Verstößen wenden Sie sich bitte an Study_golang@163 .com, um es zu löschen
Neuestes Tutorial Mehr>

Haftungsausschluss: Alle bereitgestellten Ressourcen stammen teilweise aus dem Internet. Wenn eine Verletzung Ihres Urheberrechts oder anderer Rechte und Interessen vorliegt, erläutern Sie bitte die detaillierten Gründe und legen Sie einen Nachweis des Urheberrechts oder Ihrer Rechte und Interessen vor und senden Sie ihn dann an die E-Mail-Adresse: [email protected] Wir werden die Angelegenheit so schnell wie möglich für Sie erledigen.

Copyright© 2022 湘ICP备2022001581号-3