go語言映射(map)要點總結

圖片.png

1. 定義

  • Go語言中映射是一種字典類型的數據結構,相似於c++和java中的hashmap,用於存儲一系列無序的鍵值對。
  • 映射是基於鍵來存儲值。映射的優點是可以基於鍵快速索引數據。鍵就像索引同樣,指向與該鍵關聯的值,在內存中鍵值對的關係以下圖所示。
圖片.png
  • 和切片相似,映射維護的是底層數組的指針,屬於引用類型。

2. 內部實現

  • 映射是一個集合,可使用相似處理數組和切片的方式迭代映射中的元素。但映射是無序的,不能對鍵值對進行排序。即便按順序保存,每次迭代的時候順序也不同。無序的緣由是映射的實現使用了散列表。
  • 這個散列表是由hmap實現的,hmap中維護着存儲鍵值對的bucket數組,hmap和bucket數組的關係以下圖所示。
圖片.png
  • bucket是一個鏈表結構,每個bucket後面鏈接的bucket表示bucket容量不夠用時進行擴容添加新的bucket,bucket的數據結構以下圖所示。
圖片.png
  • 說到散列表,必定要有散列函數,散列函數是散列表中存取元素的關鍵。Go語言的映射中也有這樣的散列函數(或叫哈希函數),可是Go語言把散列函數計算出的散列值能夠說是用到了極致。它把散列值分紅了高8位和低8位兩部分,以下圖所示。
圖片.png
  • 散列值的做用java

    • 低8位散列值:用於尋找當前key位於哪一個bucket;
    • 高8位散列值:用於尋找當前key位於bucket中的哪一個位置
  • 映射的存取過程:在存儲,刪除或者查找鍵值對的時候,都要先將指定的鍵傳給散列函數獲得一個散列值,而後根據散列值的低8位選中對應的桶,最終再根據桶中的索引(散列值的高8位)將鍵值對分佈到這個桶裏。隨着映射中存儲的增長,索引分佈越均勻,訪問鍵值對的速度就越快。所以,映射經過設置合理數量的桶平衡鍵值對的分佈。整個過程以下圖所示。
圖片.png
  • 鍵值對的存儲方式:鍵值對的存儲不是以key1,value1,key2,value2這樣的形式存儲,主要是爲了在key和value所佔字節長度不一樣的時候,能夠消除padding帶來的空間浪費。
  • 映射的擴容:當散列表的容量須要增加的時候,Go語言會將bucket數組的容量擴充一倍,產生新的bucket數組,並將舊數據遷移到新數組。
  • 判斷是否擴充的條件,就是散列表的加載因子,加載因子是一個閾值,表示散列表的空間利用率,Go語言map中的加載因子計算公式爲:map長度/2^B,閾值是6.5,B表示已擴容的次數。
  • 映射中數據的刪除c++

    • 若是key是指針類型的,直接將其置空,等待GC清除;
    • 若是是值類型的,則清除相關內存;
    • 對value作一樣的操做;
    • 把key對應的索引置爲空。

3. 映射的建立

(1) 使用字面量建立

// 建立一個鍵爲字符串類型,值爲整形的map
    m1 := map[string]int{"last":2019, "now":2020}
    // 獲取map中的元素
    fmt.Println(m1["last"])  // 2019
    fmt.Println(m1["now"])   // 2020
    
    // 使用字面量建立一個空map
    m2 := map[string]string{}
    fmt.Println(m2)  // map[]
映射的鍵的類型能夠是內置類型,也能夠是結構類型,但 這個類型必須可使用==運算符作比較。切片,函數以及包含切片的結構類型因爲具備引用語義,不能做爲映射的鍵,不然會形成編譯錯誤。

(2) 使用內置的make函數來建立

m1 := make(map[int] string) // map的容量使用默認值
    m1[1] = "lioney"
    m2 := make(map[int] string, 10) // map的容量使用給定值
    m2[1] = "carlos"
    fmt.Println(m1[1])  // lioney
    fmt.Println(m2[1])  // carlos

4.映射支持的操做

  • map中單個鍵值的訪問格式爲mapName[key],能夠用於獲取或更新map中的元素
  • 可使用for range遍歷map中的元素,不保證每次迭代順序一致
  • 刪除map中的某個鍵值對,使用語法delete(mapName, key)
  • 使用內置函數len()獲取map中保存的鍵值對數量,map中不支持cap()函數
package main

    import "fmt"

    func main() {
        // 建立一個空的map
        m1 := make(map[int] string)
        // 向map中添加元素
        m1[1] = "lioney"
        m1[2] = "carlos"
        m1[3] = "tom"
        // 從map中刪除鍵爲3的元素
        delete(m1, 3)
        // len()表示map中鍵值對的數量
        fmt.Println("len=", len(m1))
        // 遍歷map
        for k, v := range m1 {
            fmt.Println("key=", k, "value=", v)
        }
    }
上述代碼編譯後運行結果以下:
len= 2
    key= 2 value= carlos
    key= 1 value= lioney
    
    Process finished with exit code 0

5. 映射的使用要點

(1) 對nil映射賦值會產生運行時錯誤

和切片相似,映射在使用時必須對其底層數組進行初始化,要麼使用make進行初始化,要麼使用字面量初始化,若是隻是簡單地聲明瞭一個map,而沒有進行初始化,就是nil映射,是不能對其賦值的,請看下面代碼:
// 聲明一個map
    var colors map[string]string

    // 將red加入colors
    colors["red"] = "#da137"  // panic: assignment to entry in nil map
能夠作以下修改:
// 聲明一個map
    var colors map[string]string
    // 對map進行初始化
    colors = make(map[string]string)

    // 將red加入colors
    colors["red"] = "#da137" // no panic or error
也能夠作以下修改:
// 使用字面量建立要給空map
    colors := map[string]string{}

    // 將red加入colors
    colors["red"] = "#da137"  // no panic or error
強烈推薦使用第二種,由於用字面量建立map比較簡潔並且比較快

(2) 從映射獲取值並判斷鍵是否存在

在Go語言裏,經過鍵來索引值時,即使這個鍵不存在也會返回一個值,有時候咱們須要判斷獲取到的值是否時默認的零值,代碼以下所示。
// 使用字面量建立一個空map
    colors := map[string]string{}

    // 將red加入colors
    colors["red"] = "#da137" 
    
    // 獲取blue對應的值並判斷是否爲零值
    value1, exists := colors["blue"]
    if exists {
        fmt.Println(value1)
    }
    
    // 也能夠經過值直接判斷是否爲零值 
    value2 := colors["blue"]
    if value2 != "" {
        fmt.Println(value2)
    }

(3) 在函數間傳遞映射

package main

    import (
        "fmt"
        "unsafe"
    )

    func foo(m map[string]string) {
        // 打印參數的大小
        fmt.Println("參數大小", unsafe.Sizeof(m))
        // 刪除鍵爲green的元素
        delete(m, "green")
    }

    func main() {
        // 使用字面量建立一個空map
        colors := map[string]string{}

        // 將red加入colors
        colors["red"] = "#da137"
        colors["coral"] = "#ff7f50"
        colors["green"] = "#228b22"
        // 調用foo函數
        foo(colors)
        // 遍歷打印map
        for k, v := range colors {
            fmt.Println("key=", k, "value=", v)
        }
    }
編譯並運行以上代碼,結果以下:
參數大小 8
    key= red value= #da137
    key= coral value= #ff7f50

    Process finished with exit code 0
映射是引用類型的數據結構,在函數間傳遞映射的時候,不會拷貝映射底層的數據,從上面代碼的運行結果能夠看出,只傳遞了一個8字節大小的指針, 實際上,map類型就是一個指針類型,函數對映射的操做都是經過這個指針間接進行的,所以對映射中數據的修改,其它引用到該映射的地方也能感知到

(4) 修改映射中的結構體類型,必須總體賦值(!!!)

package main

    import "fmt"

    type User struct {
        name string
        age  int
    }

    func main() {
        m := make(map[int]User)
        user := User{
            name: "lioney",
            age:  18,
        }
        // 將user加入到map中
        m[1] = user
        
        // 修改user
        // 不能經過map引用直接修改!!!
        //m[1].age = 2 // error: cannot assign to struct field m[1].age in map

        // 必須總體替換
        user.age = 2
        m[1] = user
        fmt.Println(m)
    }

(5) 併發問題

Go語言內置的map不是併發安全的,如果在併發場景下以共享的形式訪問map中的元素,必須加鎖,要想使用併發安全的map,可使用Go語言標準庫中sync包下提供的map。

參考文獻

  1. 博客https://blog.csdn.net/i644803...
  2. William Kennedy等《Go語言實戰》第四章相關內容
  3. 李文塔《Go語言核心編程》第一章相關內容

我是lioney,年輕的後端攻城獅一枚,愛鑽研,愛技術,愛分享。
我的筆記,整理不易,感謝閱讀、點贊和收藏。
文章有任何問題歡迎你們指出,也歡迎你們一塊兒交流後端各類問題!
相關文章
相關標籤/搜索