切換語言為:簡體

圖解Go的map元素為什麼不可定址

  • 爱糖宝
  • 2024-08-28
  • 2072
  • 0
  • 0

0. 背景

相信很多Golang開發者在使用map時,都曾遇到過這樣的報錯:

m := map[string]int{"a": 1}
p := &m["a"]

執行這段程式碼時,編譯器毫不留情地報錯了:cannot take the address of m["a"]。這是為什麼呢?明明map中的元素是存在的,為什麼不能直接取地址呢?別急,這篇文章將帶你深入探討其中的原因。

1. 概述

map是Golang中非常強大的數據結構,它提供了高效的鍵值對儲存與查詢功能。然而,與陣列或切片不同的是,map中的元素是不可定址的。這意味著你無法直接獲取map中元素的指標。那麼,究竟是什麼原因導致了這一特性?這是否與map的內部結構、記憶體管理、或是編譯器的設計有關呢?接下來我們將一一解答。

2. Go中定址和不可定址的概念

要理解為什麼map元素不可定址,咱們得先搞明白什麼是定址。在Go裡,定址就是獲取變數的記憶體地址,從而允許我們直接操作該變數。能取地址的變數通常都是“穩”的,也就是說它們在記憶體中的位置不會隨便變動,比如你宣告的變數、陣列元素等等。

但有些東西是不穩的,比如函式返回值、常量,還有咱們的主角——map元素。它們在記憶體中不保證有個固定的位置,所以Go壓根兒不讓你取它們的地址,省得後續出么蛾子。

3. map元素不可定址的技術原因

要理解為什麼map元素不可定址,我們需要深入探討map的底層實現。

記憶體分配與管理

map是一種基於雜湊表的數據結構。雜湊表的特點是動態擴容,當元素增加時,原有儲存 map 元素的記憶體空間不足以儲存新的元素,那麼map會在記憶體中重新分配儲存空間。

由於這些元素在擴容過程中會被複制到新的記憶體地址上,因此它們的記憶體地址並不穩定。

舉個例子

package main

import "fmt"

func main() {
    myMap := make(map[int]string)

    // 初始填充資料
    myMap[1] = "A"
    myMap[2] = "B"
    myMap[3] = "C"
    fmt.Println("初始狀態:", myMap)

    // 假設這裏能拿到了地址
    addr1 := &myMap[1]
    
    // 新增更多資料,觸發擴容
    myMap[4] = "D"
    myMap[5] = "E"
    myMap[6] = "F"
    fmt.Println("擴容後狀態:", myMap)

    // 由於擴容了,addr1不再儲存著"A"這個字元,設想下如果此時addr1變成儲存的是作業系統級別的資料,我們這樣有許可權操作地址,將會多麼可怕!
    fmt.Println(*addr1)
    *addr1 = "OS die!"
}

圖解Go的map元素為什麼不可定址

map元素的非確定性

map中的元素儲存位置並不是固定的,尤其是在擴容、刪除元素等操作之後,某個鍵對應的值在記憶體中的位置可能會發生變化。如果允許對這些元素取地址,那麼一旦元素位置改變,原先的指標就會失效,導致不可預測的行為。

編譯器限制

爲了避免上述問題,Go編譯器設計時直接限制了對map元素的取地址操作。這樣不僅簡化了編譯器的實現,也避免了可能出現的隱患。

4. 圖解說明

記憶體佈局圖

讓我們透過一個圖示來直觀理解map在記憶體中的佈局,以及為什麼它的元素不可定址:

圖解Go的map元素為什麼不可定址

1 Map 初始狀態:

  • B = 2 表示初始時桶的數量為 4(2^B = 4)。

  • bucketArray 是一個包含 4 個 bucket 的陣列。每個 bucket 儲存了某些鍵值對。

  • 如圖所示,鍵值對 Key: 1, Value: AKey: 2, Value: BKey: 3, Value: C 分別儲存在 Bucket 0Bucket 1Bucket 2 中。

2 Map 擴容後狀態:

  • 當插入更多元素時,map 觸發擴容,B 變為 3,此時桶的數量增加到 8(2^B = 8)。

  • 現有的鍵值對重新雜湊,並被分配到新的 bucket 中。例如,Key: 1 被重新分配到新的 Bucket 0,地址從 0x01 變為 0x11。新增的元素 Key: 4, Value: D 等分別分配到新的桶中。

在擴容過程中,map中的鍵值對會被重新分配到新的雜湊桶中,因此它們的記憶體地址會發生變化。

示例程式碼

m := map[string]int{"a": 1, "b": 2}
p := &m["a"]  // 報錯:cannot take the address of m["a"]

在這個例子中,m["a"]是一個非確定性值,因此編譯器無法保證它在記憶體中的位置,所以不允許我們獲取它的地址。

5. 實踐中的影響

map元素不可定址這一特性,對實際程式設計有一定影響。特別是在處理複雜數據結構或併發操作時,開發者需要格外注意。例如,直接對map中的元素進行修改時,我們需要透過值複製、或者使用臨時變數來避免取地址的需求。

6. 解決方法與替代方案

使用臨時變數

一種常見的解決方法是使用臨時變數來儲存map元素的值,再對該臨時變數進行操作:

val := m["a"]
p := &val

雖然這樣避免了直接取地址的問題,但要注意值的同步。

利用指標儲存值

另一種方法是在map中儲存指標而非直接值:

m := map[string]*int{"a": new(int)}
*m["a"] = 1
p := m["a"]  // 這裏p就是一個指標,可以直接操作

7. 最佳實踐建議

在實際開發中,建議儘量避免對map元素的地址操作,除非有明確的需求,並且使用了合適的解決方法。此外,在需要高效儲存和訪問複雜數據結構時,可以考慮使用其他數據結構如structslice

8. 結論

在 Go 語言中,map 中的元素是不可定址的,這主要是因為以下幾個原因:

  1. 記憶體佈局不穩定:


    • 如上圖所示,map 會根據需要進行動態擴容和重新雜湊,元素可能會在記憶體中移動。因此,某個元素在一個時刻的記憶體地址在下一時刻可能會無效。

  1. 雜湊表的動態特性:


    • map 是基於雜湊表實現的,當插入或刪除元素時,map 可能會重新分配記憶體。元素在雜湊表中的位置依賴於雜湊值和 bucket 數量的變化,因此它們的地址在記憶體中不是恆定的。

  1. 編譯器限制:


    • Go 語言的編譯器透過設計禁止對 map 中元素的地址進行操作,以避免由於動態記憶體佈局而導致的潛在錯誤。這也是爲了確保程式的安全性和可預測性。

0則評論

您的電子郵件等資訊不會被公開,以下所有項目均必填

OK! You can skip this field.