Go Maps erklärt: Wie Schlüssel-Wert-Paare tatsächlich gespeichert werden

WBOY
Freigeben: 2024-09-10 11:01:50
Original
1003 Leute haben es durchsucht

Dies ist ein Auszug aus dem Beitrag; Der vollständige Beitrag ist hier verfügbar: https://victoriametrics.com/blog/go-map/.

Wenn Sie neu bei Go sind, kann es etwas verwirrend sein, herauszufinden, wie man Karten in Go verwendet. Und selbst wenn Sie mehr Erfahrung haben, kann es ziemlich schwierig sein, zu verstehen, wie Karten wirklich funktionieren.

Nehmen Sie dieses Beispiel: Haben Sie jemals einen „Hinweis“ für eine Karte festgelegt und sich gefragt, warum er „Hinweis“ heißt und nicht so einfach wie „Länge“, wie wir es bei Slices tun?

// hint = 10
m := make(map[string]int, 10)
Nach dem Login kopieren

Oder vielleicht ist Ihnen aufgefallen, dass bei der Verwendung einer For-Range-Schleife auf einer Karte die Reihenfolge nicht mit der Einfügereihenfolge übereinstimmt und sich sogar ändert, wenn Sie zu unterschiedlichen Zeiten über dieselbe Karte schleifen. Aber seltsamerweise bleibt die Reihenfolge normalerweise gleich, wenn Sie sie genau zur gleichen Zeit durchlaufen.

Das ist eine lange Geschichte, also schnallen Sie sich an und tauchen Sie ein.

Bevor wir fortfahren, nur eine Vorwarnung – die Informationen hier basieren auf Go 1.23. Wenn sich etwas geändert hat und dies nicht auf dem neuesten Stand ist, können Sie mich gerne unter X(@func25) anpingen.

Karte in Go: Schnellstart

Reden wir also über Karten in Go, es handelt sich um einen integrierten Typ, der als Schlüsselwertspeicher fungiert. Im Gegensatz zu Arrays, bei denen Sie auf Schlüssel als aufsteigende Indizes wie 0, 1, 2 usw. angewiesen sind, kann der Schlüssel bei Karten einen beliebigen vergleichbaren Typ haben.

Das gibt Ihnen viel mehr Flexibilität.

m := make(map[string]int)
m["a"] = 1
m["b"] = 2

m // map[a:1 b:2]
Nach dem Login kopieren

In diesem Beispiel haben wir mit make() eine leere Map erstellt, wobei die Schlüssel Strings und die Werte Ints sind.

Go Maps Explained: How Key-Value Pairs Are Actually Stored

Karte["a": 1, "b": 2]

Anstatt jeden Schlüssel manuell zuzuweisen, können Sie jetzt Zeit sparen, indem Sie ein Kartenliteral verwenden. Dadurch können Sie Ihre Schlüssel-Wert-Paare alle auf einmal einrichten, direkt beim Erstellen der Karte:

m := map[string]int{
    "a": 1,
    "b": 2,
}
Nach dem Login kopieren

Alles, was Sie tun müssen, ist, die Schlüssel und ihre Werte in geschweiften Klammern aufzulisten, wenn Sie die Karte zum ersten Mal erstellen. So einfach ist das.

Und wenn Sie später feststellen, dass Sie ein bestimmtes Schlüssel-Wert-Paar nicht mehr benötigen, ist Go genau das Richtige für Sie. Es gibt eine praktische Löschfunktion, die den nicht benötigten Schlüssel löscht: delete(m, "a").

Der Nullwert einer Karte ist Null, und eine Null-Karte ist in gewisser Weise wie eine leere Karte. Sie können versuchen, darin nach Schlüsseln zu suchen, und Go wird nicht ausrasten und Ihr Programm abstürzen lassen.

Wenn Sie nach einem Schlüssel suchen, der nicht vorhanden ist, gibt Ihnen Go einfach stillschweigend den „Nullwert“ für den Werttyp dieser Karte:

var m map[string]int

println(m["a"]) // 0
m["a"] = 1      // panic: assignment to entry in nil map
Nach dem Login kopieren

Aber hier ist die Sache: Sie können einer Null-Map keine neuen Schlüssel-Wert-Paare hinzufügen.

Tatsächlich behandelt Go Karten auf eine Art und Weise, die der Art und Weise, wie es mit Slices umgeht, ziemlich ähnlich ist. Sowohl Maps als auch Slices beginnen mit Null, und Go gerät nicht in Panik, wenn Sie etwas „Harmloses“ mit ihnen machen, während sie Null sind. Sie können beispielsweise einen Null-Slice durchlaufen, ohne dass es zu einem „Drama“ kommt.

Was passiert also, wenn Sie versuchen, eine Nullkarte zu durchlaufen?

var m map[string]int

for k, v := range m {
    println(k, v)
} 
Nach dem Login kopieren

Nichts passiert, keine Fehler, keine Überraschungen. Es macht einfach stillschweigend nichts.

Gos Ansatz besteht darin, den Standardwert jeglicher Art als etwas Nützliches zu behandeln und nicht als etwas, das Ihr Programm zum Absturz bringt. Das einzige Mal, dass Go einen Anfall auslöst, ist, wenn Sie etwas tun, das wirklich illegal ist, wie zum Beispiel der Versuch, ein neues Schlüssel-Wert-Paar zu einer Null-Map hinzuzufügen oder auf einen Index außerhalb der Grenzen in einem Slice zuzugreifen.

Es gibt noch ein paar weitere Dinge, die Sie über Karten in Go wissen sollten:

  • Eine for-range-Schleife über eine Karte gibt die Schlüssel nicht in einer bestimmten Reihenfolge zurück.
  • Karten sind nicht threadsicher. Die Go-Laufzeit verursacht einen schwerwiegenden Fehler, wenn Sie versuchen, gleichzeitig auf dieselbe Karte zu lesen (oder mit einem for-range zu iterieren) und zu schreiben.
  • Sie können überprüfen, ob sich ein Schlüssel in einer Karte befindet, indem Sie einen einfachen OK-Check durchführen: _, ok := m[key].
  • Der Schlüsseltyp für eine Karte muss vergleichbar sein.

Lassen Sie uns auf den letzten Punkt über Kartenschlüssel eingehen. Ich habe bereits erwähnt, dass „der Schlüssel jeder vergleichbare Typ sein könnte“, aber es steckt noch etwas mehr dahinter.

„Was genau ist also ein vergleichbarer Typ? Und was nicht?“

Es ist ganz einfach: Wenn Sie == verwenden können, um zwei Werte desselben Typs zu vergleichen, dann gilt dieser Typ als vergleichbar.

func main() {
    var s map[int]string

    if s == s {
        println("comparable")
    }
}

// compile error: invalid operation: s == s (map can only be compared to nil)
Nach dem Login kopieren

Aber wie Sie sehen können, lässt sich der obige Code nicht einmal kompilieren. Der Compiler beschwert sich: "ungültige Operation: s == s (Karte kann nur mit Null verglichen werden)."

Die gleiche Regel gilt für andere nicht vergleichbare Typen wie Slices, Funktionen oder Strukturen, die Slices oder Maps usw. enthalten. Wenn Sie also versuchen, einen dieser Typen als Schlüssel in einer Map zu verwenden, ist dies der Fall Pech gehabt.

func main() {
  var s map[[]int]string
}

// compile error: invalid map key type []intcompilerIncomparableMapKey
Nach dem Login kopieren

Aber hier ist ein kleines Geheimnis: Schnittstellen können sowohl vergleichbar als auch nicht vergleichbar sein.

Was bedeutet das? Sie können durchaus eine Map mit einer leeren Schnittstelle als Schlüssel definieren, ohne dass es zu Kompilierungsfehlern kommt. Aber Vorsicht, es besteht eine gute Chance, dass ein Laufzeitfehler auftritt.

func main() {
    m := map[interface{}]int{
        1: 1,
        "a": 2,
    }

    m[[]int{1, 2, 3}] = 3
    m[func() {}] = 4
}

// panic: runtime error: hash of unhashable type []int
// panic: runtime error: hash of unhashable type func()
Nach dem Login kopieren

Everything looks fine until you try to assign an uncomparable type as a map key.

That's when you'll hit a runtime error, which is trickier to deal with than a compile-time error. Because of this, it's usually a good idea to avoid using interface{} as a map key unless you have a solid reason and constraints that prevent misuse.

But that error message: "hash of unhashable type []int" might seem a bit cryptic. What's this about a hash? Well, that's our cue to dig into how Go handles things under the hood.

Map Anatomy

When explaining the internals of something like a map, it's easy to get bogged down in the nitty-gritty details of the Go source code. But we're going to keep it light and simple so even those new to Go can follow along.

What you see as a single map in your Go code is actually an abstraction that hides the complex details of how the data is organized. In reality, a Go map is composed of many smaller units called "buckets."

type hmap struct {
  ...
  buckets unsafe.Pointer
  ...
}
Nach dem Login kopieren

Look at Go source code above, a map contains a pointer that points to the bucket array.

This is why when you assign a map to a variable or pass it to a function, both the variable and the function's argument are sharing the same map pointer.

func changeMap(m2 map[string]int) {
  m2["hello"] = 2
}

func main() {
  m1 := map[string]int{"hello": 1}
  changeMap(m1)
  println(m1["hello"]) // 2
}
Nach dem Login kopieren

But don't get it twisted, maps are pointers to the hmap under the hood, but they aren't reference types, nor are they passed by reference like a ref argument in C#, if you change the whole map m2, it won't reflect on the original map m1 in the caller.

func changeMap(m2 map[string]int) {
  m2 = map[string]int{"hello": 2}
}

func main() {
  m1 := map[string]int{"hello": 1}
  changeMap(m1)
  println(m1["hello"]) // 1
}
Nach dem Login kopieren

In Go, everything is passed by value. What's really happening is a bit different: when you pass the map m1 to the changeMap function, Go makes a copy of the *hmap structure. So, m1 in the main() and m2 in the changeMap() function are technically different pointers point to the same hmap.

Go Maps Explained: How Key-Value Pairs Are Actually Stored

Map is passed by value

For more on this topic, there's a great post by Dave Cheney titled There is no pass-by-reference in Go.

Each of these buckets can only hold up to 8 key-value pairs, as you can see in the image below.

Go Maps Explained: How Key-Value Pairs Are Actually Stored

Buckets of a map

The map above has 2 buckets, and len(map) is 6.

So, when you add a key-value pair to a map, Go doesn't just drop it in there randomly or sequentially. Instead, it places the pair into one of these buckets based on the key's hash value, which is determined by hash(key, seed).

Let's see the simplest assignment scenario in the image below, when we have an empty map, and assign a key-value pair "hello": 1 to it.

Go Maps Explained: How Key-Value Pairs Are Actually Stored

Assign a key-value pair to an empty map

It starts by hashing "hello" to a number, then it takes that number and mods it by the number of buckets.

Since we only have one bucket here, any number mod 1 is 0, so it's going straight into bucket 0 and the same process happens when you add another key-value pair. It'll try to place it in bucket 0, and if the first slot's taken or has a different key, it'll move to the next slot in that bucket.

Take a look at the hash(key, seed), when you use a for-range loop over two maps with the same keys, you might notice that the keys come out in a different order:

func main() {
    a := map[string]int{"a": 1, "b": 2, "c": 3, "d": 4, "e": 5, "f": 6}
    b := map[string]int{"a": 1, "b": 2, "c": 3, "d": 4, "e": 5, "f": 6}

    for i := range a {
        print(i, " ")
    }
    println()

    for i := range b {
        print(i, " ")
    }
}

// Output:
// a b c d e f 
// c d e f a b     
Nach dem Login kopieren

How's that possible? Isn't the key "a" in map a and the key "a" in map b hashed the same way?

But here's the deal, while the hash function used for maps in Go is consistent across all maps with the same key type, the seed used by that hash function is different for each map instance. So, when you create a new map, Go generates a random seed just for that map.

In the example above, both a and b use the same hash function because their keys are string types, but each map has its own unique seed.

"Wait, a bucket has only 8 slots? What happens if the bucket gets full? Does it grow like a slice?"

Well, sort of. When the buckets start getting full, or even almost full, depending on the algorithm's definition of "full", the map will trigger a growth, which might double the number of main buckets.

But here's where it gets a bit more interesting.

Apabila saya menyebut "baldi utama," saya menyediakan konsep lain: "baldi limpahan." Ini berlaku apabila anda menghadapi situasi dengan perlanggaran yang tinggi. Bayangkan anda mempunyai 4 baldi, tetapi satu daripadanya dipenuhi sepenuhnya dengan 8 pasangan nilai kunci akibat perlanggaran yang tinggi, manakala 3 baldi yang lain sedang kosong.

Siaran penuh tersedia di sini: https://victoriametrics.com/blog/go-map/.

Das obige ist der detaillierte Inhalt vonGo Maps erklärt: Wie Schlüssel-Wert-Paare tatsächlich gespeichert werden. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!

Quelle:dev.to
Erklärung dieser Website
Der Inhalt dieses Artikels wird freiwillig von Internetnutzern beigesteuert und das Urheberrecht liegt beim ursprünglichen Autor. Diese Website übernimmt keine entsprechende rechtliche Verantwortung. Wenn Sie Inhalte finden, bei denen der Verdacht eines Plagiats oder einer Rechtsverletzung besteht, wenden Sie sich bitte an admin@php.cn
Beliebte Tutorials
Mehr>
Neueste Downloads
Mehr>
Web-Effekte
Quellcode der Website
Website-Materialien
Frontend-Vorlage