Peta Go Dijelaskan: Cara Pasangan Nilai Kunci Sebenarnya Disimpan

WBOY
Lepaskan: 2024-09-10 11:01:50
asal
1004 orang telah melayarinya

Ini adalah petikan siaran; siaran penuh tersedia di sini: https://victoriametrics.com/blog/go-map/.

Jika anda baru menggunakan Go, agak mengelirukan untuk memikirkan cara menggunakan peta dalam Go. Dan walaupun anda lebih berpengalaman, memahami cara peta benar-benar berfungsi boleh menjadi agak sukar.

Ambil contoh ini: Pernahkah anda menetapkan 'petunjuk' untuk peta dan tertanya-tanya mengapa ia dipanggil 'petunjuk' dan bukan sesuatu yang mudah seperti panjang, seperti yang kita lakukan dengan kepingan?

// hint = 10
m := make(map[string]int, 10)
Salin selepas log masuk

Atau mungkin anda perasan bahawa apabila anda menggunakan gelung jarak jauh pada peta, tertib itu tidak sepadan dengan tertib sisipan, malah ia berubah jika anda menggelung pada peta yang sama pada masa yang berbeza. Tetapi peliknya, jika anda mengulanginya pada masa yang sama, pesanan biasanya tetap sama.

Ini adalah cerita yang panjang, jadi kencangkan tali pinggang keledar anda dan selami.

Sebelum kita meneruskan, sekadar makluman—maklumat di sini adalah berdasarkan Go 1.23. Jika keadaan telah berubah dan ini tidak terkini, sila ping saya di X(@func25).

Peta dalam Go: Mula Pantas

Jadi, mari bercakap tentang peta dalam Go, ia adalah jenis terbina dalam yang bertindak sebagai storan nilai kunci. Tidak seperti tatasusunan yang anda terperangkap dengan kunci sebagai indeks yang semakin meningkat seperti 0, 1, 2 dan seterusnya, dengan peta, kuncinya boleh menjadi sebarang jenis yang setanding.

Itu memberikan anda lebih banyak fleksibiliti.

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

m // map[a:1 b:2]
Salin selepas log masuk

Dalam contoh itu, kami mencipta peta kosong menggunakan make(), dengan kuncinya ialah rentetan dan nilainya ialah int.

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

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

Sekarang, bukannya memberikan setiap kunci secara manual, anda boleh menjimatkan masa anda dengan menggunakan literal peta. Ini membolehkan anda menyediakan pasangan nilai kunci anda sekaligus, tepat apabila anda membuat peta:

m := map[string]int{
    "a": 1,
    "b": 2,
}
Salin selepas log masuk

Apa yang anda lakukan ialah menyenaraikan kunci dan nilainya di dalam pendakap kerinting apabila anda mula-mula mencipta peta, semudah itu.

Dan jika anda menyedari kemudian bahawa anda tidak memerlukan pasangan nilai kunci tertentu lagi, Go akan membantu anda. Terdapat fungsi padam yang berguna yang, baik, memadamkan kunci yang anda tidak mahu: padam(m, "a").

Nilai sifar peta ialah sifar dan peta sifar adalah seperti peta kosong dalam beberapa cara. Anda boleh cuba mencari kunci di dalamnya, dan Go tidak akan panik dan ranap program anda.

Jika anda mencari kunci yang tiada di sana, Go secara senyap-senyap memberikan anda "nilai sifar" untuk jenis nilai peta itu:

var m map[string]int

println(m["a"]) // 0
m["a"] = 1      // panic: assignment to entry in nil map
Salin selepas log masuk

Tetapi inilah perkaranya: anda tidak boleh menambah pasangan nilai kunci baharu pada peta sifar.

Malah, Go mengendalikan peta dengan cara yang hampir sama dengan cara ia menangani kepingan. Kedua-dua peta dan kepingan bermula sebagai sifar, dan Go tidak panik jika anda melakukan sesuatu yang "tidak berbahaya" dengan mereka semasa mereka tiada. Sebagai contoh, anda boleh melingkari sekeping nol tanpa sebarang "drama".

Jadi, apa yang berlaku jika anda cuba mengulangi peta kosong?

var m map[string]int

for k, v := range m {
    println(k, v)
} 
Salin selepas log masuk

Tiada apa-apa yang berlaku, tiada ralat, tiada kejutan. Ia secara senyap-senyap tidak melakukan apa-apa.

Pendekatan Go adalah untuk menganggap nilai lalai apa-apa jenis sebagai sesuatu yang berguna, bukan sesuatu yang menyebabkan program anda meledak. Satu-satunya masa Go membuat kesesuaian ialah apabila anda melakukan sesuatu yang benar-benar menyalahi undang-undang, seperti cuba menambah pasangan nilai kunci baharu pada peta sifar atau mengakses indeks di luar sempadan dalam kepingan.

Terdapat beberapa perkara lagi yang perlu anda ketahui tentang peta dalam Go:

  • Gelung jarak jauh di atas peta tidak akan mengembalikan kekunci dalam sebarang susunan tertentu.
  • Peta tidak selamat untuk benang, masa jalan Go akan menyebabkan ralat maut jika anda cuba membaca (atau mengulang dengan jarak jauh) dan menulis pada peta yang sama secara serentak.
  • Anda boleh menyemak sama ada kunci berada dalam peta dengan melakukan semakan ok mudah: _, ok := m[kunci].
  • Jenis kunci untuk peta mestilah setanding.

Mari kita menyelami perkara terakhir tentang kunci peta. Saya telah menyebut sebelum ini bahawa "kunci itu boleh menjadi apa-apa jenis yang setanding," tetapi terdapat lebih banyak daripada itu.

"Jadi, apakah sebenarnya jenis yang setanding? Dan apa yang tidak?"

Ia agak mudah: jika anda boleh menggunakan == untuk membandingkan dua nilai daripada jenis yang sama, maka jenis itu dianggap setanding.

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)
Salin selepas log masuk

Tetapi seperti yang anda lihat, kod di atas tidak dikompil. Pengkompil mengadu: "operasi tidak sah: s == s (peta hanya boleh dibandingkan dengan sifar)."

Peraturan yang sama ini digunakan untuk jenis lain yang tidak setanding seperti kepingan, fungsi atau struktur yang mengandungi kepingan atau peta, dsb. Jadi, jika anda cuba menggunakan mana-mana jenis tersebut sebagai kunci dalam peta, anda kurang beruntung.

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

// compile error: invalid map key type []intcompilerIncomparableMapKey
Salin selepas log masuk

Tetapi inilah sedikit rahsia, antara muka boleh setanding dan tidak setanding.

Apakah maksudnya? Anda benar-benar boleh menentukan peta dengan antara muka kosong sebagai kunci tanpa sebarang ralat penyusunan. Tetapi berhati-hati, ada kemungkinan besar anda akan menghadapi ralat masa jalan.

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()
Salin selepas log masuk

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
  ...
}
Salin selepas log masuk

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
}
Salin selepas log masuk

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
}
Salin selepas log masuk

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     
Salin selepas log masuk

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.

Wenn ich „Haupt-Buckets“ sage, beziehe ich mich auf ein anderes Konzept: „Überlauf-Buckets“. Diese kommen ins Spiel, wenn es zu einer Situation mit vielen Kollisionen kommt. Stellen Sie sich vor, Sie haben 4 Buckets, aber einer davon ist aufgrund hoher Kollisionen vollständig mit 8 Schlüssel-Wert-Paaren gefüllt, während die anderen 3 Buckets leer bleiben.

Der vollständige Beitrag ist hier verfügbar: https://victoriametrics.com/blog/go-map/.

Atas ialah kandungan terperinci Peta Go Dijelaskan: Cara Pasangan Nilai Kunci Sebenarnya Disimpan. Untuk maklumat lanjut, sila ikut artikel berkaitan lain di laman web China PHP!

sumber:dev.to
Kenyataan Laman Web ini
Kandungan artikel ini disumbangkan secara sukarela oleh netizen, dan hak cipta adalah milik pengarang asal. Laman web ini tidak memikul tanggungjawab undang-undang yang sepadan. Jika anda menemui sebarang kandungan yang disyaki plagiarisme atau pelanggaran, sila hubungi admin@php.cn
Tutorial Popular
Lagi>
Muat turun terkini
Lagi>
kesan web
Kod sumber laman web
Bahan laman web
Templat hujung hadapan