亚洲乱码中文字幕综合,中国熟女仑乱hd,亚洲精品乱拍国产一区二区三区,一本大道卡一卡二卡三乱码全集资源,又粗又黄又硬又爽的免费视频

Go?復(fù)合類型之字典類型使用教程示例

 更新時間:2023年10月11日 09:14:31   作者:賈維斯Echo  
這篇文章主要為大家介紹了Go復(fù)合類型之字典類型使用教程示例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪

一、map類型介紹

1.1 什么是 map 類型?

map 是 Go 語言提供的一種抽象數(shù)據(jù)類型,它表示一組無序的鍵值對。用 key 和 value 分別代表 map 的鍵和值。而且,map 集合中每個 key 都是唯一的:

和切片類似,作為復(fù)合類型的 map,它在 Go 中的類型表示也是由 key 類型與 value 類型組成的,就像下面代碼:

map[key_type]value_type

key 與 value 的類型可以相同,也可以不同:

map[string]string // key與value元素的類型相同
map[int]string    // key與value元素的類型不同

如果兩個 map 類型的 key 元素類型相同,value 元素類型也相同,那么我們可以說它們是同一個 map 類型,否則就是不同的 map 類型。

這里,我們要注意,map 類型對 value 的類型沒有限制,但是對 key 的類型卻有嚴(yán)格要求,因?yàn)?map 類型要保證 key 的唯一性因此在這里,你一定要注意:函數(shù)類型、map 類型自身,以及切片類型是不能作為 map 的 key 類型的。比如下面這段代碼:

// 函數(shù)類型不能作為key,因?yàn)楹瘮?shù)類型是不可比較的
func keyFunc() {}
m := make(map[string]int)
m[keyFunc] = 1 // 編譯錯誤
// map類型不能作為key
m1 := make(map[string]int)
m[m1] = 1 // 編譯錯誤
// 切片類型不能作為key,因?yàn)榍衅强勺冮L度的,它們的內(nèi)容可能會在運(yùn)行時更改
s1 := []int{1,2,3}  
m[s1] = 1 // 編譯錯誤

上面代碼中,試圖使用函數(shù)類型、map類型和切片類型作為key都會導(dǎo)致編譯錯誤。

這是因?yàn)镚o語言在實(shí)現(xiàn)map時,需要比較key是否相等,因此key需要支持==比較。但函數(shù)、map和切片類型的相等性比較涉及內(nèi)存地址,無法簡單判斷,所以不能作為key。所以,key 的類型必須支持“==”和“!=”兩種比較操作符。

還需要注意的是,在 Go 語言中,函數(shù)類型、map 類型自身,以及切片只支持與 nil 的比較,而不支持同類型兩個變量的比較。如果像下面代碼這樣,進(jìn)行這些類型的比較,Go 編譯器將會報錯:

s1 := make([]int, 1)
s2 := make([]int, 2)
f1 := func() {}
f2 := func() {}
m1 := make(map[int]string)
m2 := make(map[int]string)
println(s1 == s2) // 錯誤:invalid operation: s1 == s2 (slice can only be compared to nil)
println(f1 == f2) // 錯誤:invalid operation: f1 == f2 (func can only be compared to nil)
println(m1 == m2) // 錯誤:invalid operation: m1 == m2 (map can only be compared to nil)

1.2 map 類型特性

在Go中,map具有以下特性:

  • 無序性map中的鍵值對沒有固定的順序,遍歷時可能不按照添加的順序返回鍵值對。
  • 動態(tài)增長map是動態(tài)的,它會根據(jù)需要自動增長以容納更多的鍵值對,不需要預(yù)先指定大小。
  • 零值: 如果未初始化一個map,它將是nil,并且不能存儲鍵值對。需要使用make函數(shù)來初始化一個map
  • 鍵的唯一性: 在同一個map中,每個鍵只能出現(xiàn)一次。如果嘗試使用相同的鍵插入多次,新值將覆蓋舊值。
  • 查詢效率高map的查詢操作通常非???,因?yàn)樗褂霉1韥泶鎯?shù)據(jù),這使得通過鍵查找值的時間復(fù)雜度接近常數(shù)。
  • 引用類型map是一種引用類型,多個變量可以引用并共享同一個map實(shí)例。

二.map 變量的聲明和初始化

和切片一樣,為 map 類型變量顯式賦值有兩種方式:一種是使用復(fù)合字面值;另外一種是使用 make 這個預(yù)聲明的內(nèi)置函數(shù)。

2.1 方法一:使用 make 函數(shù)聲明和初始化(推薦)

這是最常見和推薦的方式,特別是在需要在map中添加鍵值對之前初始化map的情況下。使用make函數(shù)可以為map分配內(nèi)存并進(jìn)行初始化。

// 使用 make 函數(shù)聲明和初始化 map
myMap := make(map[keyType]valueType,capacity)

其中:

  • keyType 是鍵的類型。
  • valueType 是值的類型。
  • capacity表示map的初始容量,它是可選的,可以省略不寫。

例如:和切片通過 make 進(jìn)行初始化一樣,通過 make 的初始化方式,我們可以為 map 類型變量指定鍵值對的初始容量,但無法進(jìn)行具體的鍵值對賦值,就像下面代碼這樣:

// 創(chuàng)建一個存儲整數(shù)到字符串的映射
    m1 := make(map[int]string) // 未指定初始容量
    m1[1] = "key"
    fmt.Println(m1)

map 類型的容量不會受限于它的初始容量值,當(dāng)其中的鍵值對數(shù)量超過初始容量后,Go 運(yùn)行時會自動增加 map 類型的容量,保證后續(xù)鍵值對的正常插入,比如下面這段代碼:

m2 := make(map[int]string, 2) // 指定初始容量為2
    m2[1] = "One"
    m2[2] = "Two"
    m2[3] = "Three"
    fmt.Println(m2) // 輸出:map[1:One 2:Two 3:Three] ,并不會報錯
    fmt.Println(len(m2)) // 此時,map容量已經(jīng)變?yōu)?

總結(jié):使用make函數(shù)初始化的map是空的,需要在后續(xù)代碼中添加鍵值對。

mm := make(map[int]string)
    fmt.Println(mm) // 輸出 map[]

2.2 方法二:使用復(fù)合字面值聲明初始化 map 類型變量

和切片類型變量一樣,如果我們沒有顯式地賦予 map 變量初值,map 類型變量的默認(rèn)值為 nil,比如,我們來看下面這段代碼:

var m map[string]int

if m == nil {
    fmt.Println("Map is nil")
} else {
    fmt.Println("Map is not nil")
}

不過切片變量和 map 變量在這里也有些不同。初值為零值 nil 的切片類型變量,可以借助內(nèi)置的 append 的函數(shù)進(jìn)行操作,這種在 Go 語言中被稱為“零值可用”。定義“零值可用”的類型,可以提升我們開發(fā)者的使用體驗(yàn),我們不用再擔(dān)心變量的初始狀態(tài)是否有效。比如,創(chuàng)建一個存儲字符串到整數(shù)的映射,但 map 類型,因?yàn)樗鼉?nèi)部實(shí)現(xiàn)的復(fù)雜性,無法“零值可用”。所以,如果我們對處于零值狀態(tài)的 map 變量直接進(jìn)行操作,就會導(dǎo)致運(yùn)行時異常(panic),從而導(dǎo)致程序進(jìn)程異常退出:

var m map[string]int // m = nil
m["key"] = 1         // 發(fā)生運(yùn)行時異常:panic: assignment to entry in nil map

所以,我們必須對 map 類型變量進(jìn)行顯式初始化后才能使用。我們先來看這句代碼:

m := map[int]string{}

這里,我們顯式初始化了 map 類型變量 m。不過,你要注意,雖然此時 map 類型變量 m 中沒有任何鍵值對,但變量 m 也不等同于初值為 nil 的 map 變量。這個時候,我們對 m 進(jìn)行鍵值對的插入操作,不會引發(fā)運(yùn)行時異常。

這里我們再看看怎么通過稍微復(fù)雜一些的復(fù)合字面值,對 map 類型變量進(jìn)行初始化:

m1 := map[int][]string{
    1: []string{"val1_1", "val1_2"},
    3: []string{"val3_1", "val3_2", "val3_3"},
    7: []string{"val7_1"},
}

type Position struct { 
    x float64 
    y float64
}

m2 := map[Position]string{
    Position{29.935523, 52.568915}: "school",
    Position{25.352594, 113.304361}: "shopping-mall",
    Position{73.224455, 111.804306}: "hospital",
}

我們看到,上面代碼雖然完成了對兩個 map 類型變量 m1 和 m2 的顯式初始化,但不知道你有沒有發(fā)現(xiàn)一個問題,作為初值的字面值似乎有些“臃腫”。你看,作為初值的字面值采用了復(fù)合類型的元素類型,而且在編寫字面值時還帶上了各自的元素類型,比如作為 map[int] []string 值類型的[]string,以及作為 map[Position]string 的 key 類型的 Position。

別急!針對這種情況,Go 提供了“語法糖”。這種情況下,Go 允許省略字面值中的元素類型。因?yàn)?map 類型表示中包含了 key 和 value 的元素類型,Go 編譯器已經(jīng)有足夠的信息,來推導(dǎo)出字面值中各個值的類型了。我們以 m2 為例,這里的顯式初始化代碼和上面變量 m2 的初始化代碼是等價的:

m2 := map[Position]string{
    {29.935523, 52.568915}: "school",
    {25.352594, 113.304361}: "shopping-mall",
    {73.224455, 111.804306}: "hospital",
}

綜上,這種方式通常用于創(chuàng)建具有初始值的map。在這種情況下,不需要使用make函數(shù)。map的聲明方式如下:

// 使用字面量聲明和初始化 map
myMap := map[keyType]valueType{
    key1: value1,
    key2: value2,
    // ...
}

其中:

  • keyType 是鍵的類型
  • valueType 是值的類型
  • 然后使用大括號 {} 包圍鍵值對

三.map 變量的傳遞開銷(map是引用傳遞)

和切片類型一樣,map 也是引用類型。這就意味著 map 類型變量作為參數(shù)被傳遞給函數(shù)或方法的時候,實(shí)質(zhì)上傳遞的只是一個“描述符”,而不是整個 map 的數(shù)據(jù)拷貝,所以這個傳遞的開銷是固定的,而且也很小。

并且,當(dāng) map 變量被傳遞到函數(shù)或方法內(nèi)部后,我們在函數(shù)內(nèi)部對 map 類型參數(shù)的修改在函數(shù)外部也是可見的。比如你從這個示例中就可以看到,函數(shù) foo 中對 map 類型變量 m 進(jìn)行了修改,而這些修改在 foo 函數(shù)外也可見。

package main
import "fmt"
func foo(m map[string]int) {
    m["key1"] = 11
    m["key2"] = 12
}
func main() {
    m := map[string]int{
        "key1": 1,
        "key2": 2,
    }
    fmt.Println(m) // map[key1:1 key2:2]  
    foo(m)
    fmt.Println(m) // map[key1:11 key2:12] 
}

所以,map 引用類型。當(dāng) map 被賦值為一個新變量的時候,它們指向同一個內(nèi)部數(shù)據(jù)結(jié)構(gòu)。因此,當(dāng)改變其中一個變量,就會影響到另一變量。

四.map 的內(nèi)部實(shí)現(xiàn)

4.1 map 類型在 Go 運(yùn)行時層實(shí)現(xiàn)的示意圖

和切片相比,map 類型的內(nèi)部實(shí)現(xiàn)要更加復(fù)雜。Go 運(yùn)行時使用一張哈希表來實(shí)現(xiàn)抽象的 map 類型。運(yùn)行時實(shí)現(xiàn)了 map 類型操作的所有功能,包括查找、插入、刪除等。在編譯階段,Go 編譯器會將 Go 語法層面的 map 操作,重寫成運(yùn)行時對應(yīng)的函數(shù)調(diào)用。大致的對應(yīng)關(guān)系是這樣的:

// 創(chuàng)建map類型變量實(shí)例
m := make(map[keyType]valType, capacityhint) → m := runtime.makemap(maptype, capacityhint, m)
// 插入新鍵值對或給鍵重新賦值
m["key"] = "value" → v := runtime.mapassign(maptype, m, "key") v是用于后續(xù)存儲value的空間的地址
// 獲取某鍵的值 
v := m["key"]      → v := runtime.mapaccess1(maptype, m, "key")
v, ok := m["key"]  → v, ok := runtime.mapaccess2(maptype, m, "key")
// 刪除某鍵
delete(m, "key")   → runtime.mapdelete(maptype, m, “key”)

這是 map 類型在 Go 運(yùn)行時層實(shí)現(xiàn)的示意圖:

我們可以看到,和切片的運(yùn)行時表示圖相比,map 的實(shí)現(xiàn)示意圖顯然要復(fù)雜得多。接下來,我們結(jié)合這張圖來簡要描述一下 map 在運(yùn)行時層的實(shí)現(xiàn)原理。接下來我們來看一下一個 map 變量在初始狀態(tài)、進(jìn)行鍵值對操作后,以及在并發(fā)場景下的 Go 運(yùn)行時層的實(shí)現(xiàn)原理。

4.2 初始狀態(tài)

從圖中我們可以看到,與語法層面 map 類型變量(m)一一對應(yīng)的是 *runtime.hmap 的實(shí)例,即 runtime.hmap 類型的指針,也就是我們前面在講解 map 類型變量傳遞開銷時提到的 map 類型的描述符。hmap 類型是 map 類型的頭部結(jié)構(gòu)(header),它存儲了后續(xù) map 類型操作所需的所有信息,包括:

真正用來存儲鍵值對數(shù)據(jù)的是桶,也就是 bucket,每個 bucket 中存儲的是 Hash 值低 bit 位數(shù)值相同的元素,默認(rèn)的元素個數(shù)為 BUCKETSIZE(值為 8,Go 1.17 版本中在 $GOROOT/src/cmd/compile/internal/reflectdata/reflect.go 中定義,與 runtime/map.go 中常量 bucketCnt 保持一致)。

當(dāng)某個 bucket(比如 buckets[0]) 的 8 個空槽 slot)都填滿了,且 map 尚未達(dá)到擴(kuò)容的條件的情況下,運(yùn)行時會建立 overflow bucket,并將這個 overflow bucket 掛在上面 bucket(如 buckets[0])末尾的 overflow 指針上,這樣兩個 buckets 形成了一個鏈表結(jié)構(gòu),直到下一次 map 擴(kuò)容之前,這個結(jié)構(gòu)都會一直存在。

從圖中我們可以看到,每個 bucket 由三部分組成,從上到下分別是 tophash 區(qū)域、key 存儲區(qū)域和 value 存儲區(qū)域。

4.3 tophash 區(qū)域

當(dāng)我們向 map 插入一條數(shù)據(jù),或者是從 map 按 key 查詢數(shù)據(jù)的時候,運(yùn)行時都會使用哈希函數(shù)對 key 做哈希運(yùn)算,并獲得一個哈希值(hashcode)。這個 hashcode 非常關(guān)鍵,運(yùn)行時會把 hashcode“一分為二”來看待,其中低位區(qū)的值用于選定 bucket,高位區(qū)的值用于在某個 bucket 中確定 key 的位置。我把這一過程整理成了下面這張示意圖,你理解起來可以更直觀:

因此,每個 bucket 的 tophash 區(qū)域其實(shí)是用來快速定位 key 位置的,這樣就避免了逐個 key 進(jìn)行比較這種代價較大的操作。尤其是當(dāng) key 是 size 較大的字符串類型時,好處就更突出了。這是一種以空間換時間的思路。

4.4 key 存儲區(qū)域

接著,我們看 tophash 區(qū)域下面是一塊連續(xù)的內(nèi)存區(qū)域,存儲的是這個 bucket 承載的所有 key 數(shù)據(jù)。運(yùn)行時在分配 bucket 的時候需要知道 key 的 Size。那么運(yùn)行時是如何知道 key 的 size 的呢?

當(dāng)我們聲明一個 map 類型變量,比如 var m map[string]int 時,Go 運(yùn)行時就會為這個變量對應(yīng)的特定 map 類型,生成一個 runtime.maptype 實(shí)例。如果這個實(shí)例已經(jīng)存在,就會直接復(fù)用。maptype 實(shí)例的結(jié)構(gòu)是這樣的:

type maptype struct {
    typ        _type
    key        *_type
    elem       *_type
    bucket     *_type // internal type representing a hash bucket
    keysize    uint8  // size of key slot
    elemsize   uint8  // size of elem slot
    bucketsize uint16 // size of bucket
    flags      uint32
}

我們可以看到,這個實(shí)例包含了我們需要的 map 類型中的所有"元信息"。我們前面提到過,編譯器會把語法層面的 map 操作重寫成運(yùn)行時對應(yīng)的函數(shù)調(diào)用,這些運(yùn)行時函數(shù)都有一個共同的特點(diǎn),那就是第一個參數(shù)都是 maptype 指針類型的參數(shù)。

Go 運(yùn)行時就是利用 maptype 參數(shù)中的信息確定 key 的類型和大小的。map 所用的 hash 函數(shù)也存放在 maptype.key.alg.hash(key, hmap.hash0) 中。同時 maptype 的存在也讓 Go 中所有 map 類型都共享一套運(yùn)行時 map 操作函數(shù),而不是像 C++ 那樣為每種 map 類型創(chuàng)建一套 map 操作函數(shù),這樣就節(jié)省了對最終二進(jìn)制文件空間的占用。

4.5 value 存儲區(qū)域

我們再接著看 key 存儲區(qū)域下方的另外一塊連續(xù)的內(nèi)存區(qū)域,這個區(qū)域存儲的是 key 對應(yīng)的 value。和 key 一樣,這個區(qū)域的創(chuàng)建也是得到了 maptype 中信息的幫助。Go 運(yùn)行時采用了把 key 和 value 分開存儲的方式,而不是采用一個 kv 接著一個 kv 的 kv 緊鄰方式存儲,這帶來的其實(shí)是算法上的復(fù)雜性,但卻減少了因內(nèi)存對齊帶來的內(nèi)存浪費(fèi)。

我們以 map[int8]int64 為例,看看下面的存儲空間利用率對比圖:

你會看到,當(dāng)前 Go 運(yùn)行時使用的方案內(nèi)存利用效率很高,而 kv 緊鄰存儲的方案在 map[int8]int64 這樣的例子中內(nèi)存浪費(fèi)十分嚴(yán)重,它的內(nèi)存利用率是 72/128=56.25%,有近一半的空間都浪費(fèi)掉了。

另外,還有一點(diǎn)我要跟你強(qiáng)調(diào)一下,如果 key 或 value 的數(shù)據(jù)長度大于一定數(shù)值,那么運(yùn)行時不會在 bucket 中直接存儲數(shù)據(jù),而是會存儲 key 或 value 數(shù)據(jù)的指針。目前 Go 運(yùn)行時定義的最大 key 和 value 的長度是這樣的:

// $GOROOT/src/runtime/map.go
const (
    maxKeySize  = 128
    maxElemSize = 128
)

五.map 擴(kuò)容

我們前面提到過,map 會對底層使用的內(nèi)存進(jìn)行自動管理。因此,在使用過程中,當(dāng)插入元素個數(shù)超出一定數(shù)值后,map 一定會存在自動擴(kuò)容的問題,也就是怎么擴(kuò)充 bucket 的數(shù)量,并重新在 bucket 間均衡分配數(shù)據(jù)的問題。

那么 map 在什么情況下會進(jìn)行擴(kuò)容呢?Go 運(yùn)行時的 map 實(shí)現(xiàn)中引入了一個 LoadFactor(負(fù)載因子),當(dāng) count > LoadFactor * 2^B 或 overflow bucket 過多時,運(yùn)行時會自動對 map 進(jìn)行擴(kuò)容。目前 Go 1.17 版本 LoadFactor 設(shè)置為 6.5(loadFactorNum/loadFactorDen)。這里是 Go 中與 map 擴(kuò)容相關(guān)的部分源碼:

// $GOROOT/src/runtime/map.go
const (
  ... ...
  loadFactorNum = 13
  loadFactorDen = 2
  ... ...
)
func mapassign(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer {
  ... ...
  if !h.growing() && (overLoadFactor(h.count+1, h.B) || tooManyOverflowBuckets(h.noverflow, h.B)) {
    hashGrow(t, h)
    goto again // Growing the table invalidates everything, so try again
  }
  ... ...
}

這兩方面原因?qū)е碌臄U(kuò)容,在運(yùn)行時的操作其實(shí)是不一樣的。如果是因?yàn)?overflow bucket 過多導(dǎo)致的“擴(kuò)容”,實(shí)際上運(yùn)行時會新建一個和現(xiàn)有規(guī)模一樣的 bucket 數(shù)組,然后在 assign 和 delete 時做排空和遷移。

如果是因?yàn)楫?dāng)前數(shù)據(jù)數(shù)量超出 LoadFactor 指定水位而進(jìn)行的擴(kuò)容,那么運(yùn)行時會建立一個兩倍于現(xiàn)有規(guī)模的 bucket 數(shù)組,但真正的排空和遷移工作也是在 assign 和 delete 時逐步進(jìn)行的。原 bucket 數(shù)組會掛在 hmap 的 oldbuckets 指針下面,直到原 buckets 數(shù)組中所有數(shù)據(jù)都遷移到新數(shù)組后,原 buckets 數(shù)組才會被釋放。你可以結(jié)合下面的 map 擴(kuò)容示意圖來理解這個過程,這會讓你理解得更深刻一些:

六.map 與并發(fā)

接著我們來看一下 map 和并發(fā)。從上面的實(shí)現(xiàn)原理來看,充當(dāng) map 描述符角色的 hmap 實(shí)例自身是有狀態(tài)的(hmap.flags),而且對狀態(tài)的讀寫是沒有并發(fā)保護(hù)的。所以說 map 實(shí)例不是并發(fā)寫安全的,也不支持并發(fā)讀寫。如果我們對 map 實(shí)例進(jìn)行并發(fā)讀寫,程序運(yùn)行時就會拋出異常。你可以看看下面這個并發(fā)讀寫 map 的例子:

package main
import (
    "fmt"
    "time"
)
func doIteration(m map[int]int) {
    for k, v := range m {
        _ = fmt.Sprintf("[%d, %d] ", k, v)
    }
}
func doWrite(m map[int]int) {
    for k, v := range m {
        m[k] = v + 1
    }
}
func main() {
    m := map[int]int{
        1: 11,
        2: 12,
        3: 13,
    }
    go func() {
        for i := 0; i < 1000; i++ {
            doIteration(m)
        }
    }()
    go func() {
        for i := 0; i < 1000; i++ {
            doWrite(m)
        }
    }()
    time.Sleep(5 * time.Second)
}

運(yùn)行這個示例程序,我們會得到下面的執(zhí)行錯誤結(jié)果:

fatal error: concurrent map iteration and map write

不過,如果我們僅僅是進(jìn)行并發(fā)讀,map 是沒有問題的。而且,Go 1.9 版本中引入了支持并發(fā)寫安全的 sync.Map 類型,可以在并發(fā)讀寫的場景下替換掉 map。如果你有這方面的需求,可以查看一下sync.Map 的手冊。

另外,你要注意,考慮到 map 可以自動擴(kuò)容,map 中數(shù)據(jù)元素的 value 位置可能在這一過程中發(fā)生變化,所以 Go 不允許獲取 map 中 value 的地址,這個約束是在編譯期間就生效的。下面這段代碼就展示了 Go 編譯器識別出獲取 map 中 value 地址的語句后,給出的編譯錯誤:

p := &m[key]  // cannot take the address of m[key]
fmt.Println(p)

七、map 的基本操作

7.1 修改和更新鍵值對

首先 nil 的 map 類型變量,我們可以在其中插入符合 map 類型定義的任意新鍵值對。插入新鍵值對只需要把 value 賦值給 map 中對應(yīng)的 key 就可以了:

// 創(chuàng)建并初始化一個 map
myMap := make(map[string]int)
myMap["apple"] = 1
myMap["banana"] = 2

不需要自己判斷數(shù)據(jù)有沒有插入成功,因?yàn)?Go 會保證插入總是成功的。不過,如果我們插入新鍵值對的時候,某個 key 已經(jīng)存在于 map 中了,那我們的插入操作就會用新值覆蓋舊值:

// 修改鍵 "apple" 對應(yīng)的值
myMap["apple"] = 3
// 更新鍵 "cherry" 對應(yīng)的值,如果鍵不存在則創(chuàng)建新鍵值對
myMap["cherry"] = 4
// 打印修改后的 map
fmt.Println(myMap) // 輸出: map[apple:3 banana:2 cherry:4]

從這段代碼中,您可以看到如何執(zhí)行以下操作:

  • 修改鍵 "apple" 對應(yīng)的值:使用myMap["apple"] = 3這行代碼,將鍵 "apple" 對應(yīng)的值從原來的 1 修改為 3。
  • 更新鍵 "cherry" 對應(yīng)的值:使用myMap["cherry"] = 4這行代碼,更新了鍵 "cherry" 對應(yīng)的值為 4。如果鍵 "cherry" 不存在于map中,這行代碼會創(chuàng)建一個新的鍵值對。
  • 打印修改后的 map:最后使用fmt.Println(myMap)打印整個修改后的map,以顯示更新后的鍵值對。

7.2 批量更新和修改(合并同類型map)

在Go中,可以使用循環(huán)遍歷另一個map,然后使用遍歷的鍵值對來批量更新或修改目標(biāo)map的鍵值對。以下是一個實(shí)現(xiàn)類似于Python字典的update()方法的步驟:

  • 創(chuàng)建一個目標(biāo)map,它將被更新或修改。
  • 創(chuàng)建一個源map,其中包含要合并到目標(biāo)map的鍵值對。
  • 遍歷源map的鍵值對。
  • 對于每個鍵值對,檢查它是否存在于目標(biāo)map中。

    • 如果存在,將目標(biāo)map中的值更新為源map中的值。
    • 如果不存在,將源map中的鍵值對添加到目標(biāo)map中。
  • 最終,目標(biāo)map將包含源map中的所有鍵值對以及更新后的值。

以下是具體的Go代碼示例:

package main
import (
    "fmt"
)
func updateMap(target map[string]int, source map[string]int) {
    for key, value := range source {
        target[key] = value
    }
}
func main() {
    // 創(chuàng)建目標(biāo) map
    targetMap := map[string]int{
        "apple":  1,
        "banana": 2,
    }
    // 創(chuàng)建源 map,包含要更新或修改的鍵值對
    sourceMap := map[string]int{
        "apple":  3, // 更新 "apple" 的值為 3
        "cherry": 4, // 添加新的鍵值對 "cherry": 4
    }
    // 調(diào)用 updateMap 函數(shù),將源 map 合并到目標(biāo) map 中
    updateMap(targetMap, sourceMap)
    // 打印更新后的目標(biāo) map
    fmt.Println(targetMap) // 輸出:map[apple:3 banana:2 cherry:4]
}

7.3 獲取鍵值對數(shù)量

要獲取一個map中鍵值對的數(shù)量(也稱為長度),可以使用Go語言的len函數(shù)。len函數(shù)返回map中鍵值對的數(shù)量。以下是獲取map中鍵值對數(shù)量的示例:

// 創(chuàng)建并初始化一個 map
    myMap := map[string]int{
        "apple":  1,
        "banana": 2,
        "cherry": 3,
    }
    // 使用 len 函數(shù)獲取 map 的鍵值對數(shù)量
    count := len(myMap)
    // 打印鍵值對數(shù)量
    fmt.Println("鍵值對數(shù)量:", count)

不過,這里要注意的是我們不能對 map 類型變量調(diào)用 cap,來獲取當(dāng)前容量,這是 map 類型與切片類型的一個不同點(diǎn)。

7.4 查找和數(shù)據(jù)讀?。ㄅ袛嗄硞€鍵是否存在)

7.4.1 查找和數(shù)據(jù)讀取 map 語法格式

Go語言中有個判斷map中鍵是否存在的特殊寫法,格式如下:

value, ok := map[key]

其中:

  • myMap 是目標(biāo)map,您希望在其中查找鍵。
  • key 是您要查找的鍵。
  • value 是一個變量,如果鍵存在,它將存儲鍵對應(yīng)的值,如果鍵不存在,則會獲得值類型的零值。
  • ok 是一個布爾值,用于指示鍵是否存在。如果鍵存在,oktrue;如果鍵不存在,okfalse。

map 類型更多用在查找和數(shù)據(jù)讀取場合。所謂查找,就是判斷某個 key 是否存在于某個 map 中。Go 語言的 map 類型支持通過用一種名為“comma ok”的慣用法,進(jìn)行對某個 key 的查詢。接下來我們就用“comma ok”慣用法改造一下上面的代碼:

m := make(map[string]int)
v, ok := m["key1"]
if !ok {
    // "key1"不在map中
}
// "key1"在map中,v將被賦予"key1"鍵對應(yīng)的value

我們看到,這里我們通過了一個布爾類型變量 ok,來判斷鍵“key1”是否存在于 map 中。如果存在,變量 v 就會被正確地賦值為鍵“key1”對應(yīng)的 value。

不過,如果我們并不關(guān)心某個鍵對應(yīng)的 value,而只關(guān)心某個鍵是否在于 map 中,我們可以使用空標(biāo)識符替代變量 v,忽略可能返回的 value:

m := make(map[string]int)
_, ok := m["key1"]
... ...

因此,你一定要記住:在 Go 語言中,請使用“comma ok”慣用法對 map 進(jìn)行鍵查找和鍵值讀取操作。

7.4.2 實(shí)現(xiàn)get 方法查找map 對應(yīng)的key

在Go中,要實(shí)現(xiàn)類似Python字典的get()方法,可以編寫一個函數(shù),該函數(shù)接受一個map、一個鍵以及一個默認(rèn)值作為參數(shù)。函數(shù)將嘗試從map中獲取指定鍵的值,如果鍵不存在,則返回默認(rèn)值。以下是實(shí)現(xiàn)類似get()方法的步驟:

  • 創(chuàng)建一個函數(shù),命名為get,該函數(shù)接受三個參數(shù):map、鍵和默認(rèn)值。
  • 在函數(shù)中,使用鍵來嘗試從map中獲取對應(yīng)的值。
  • 如果值存在,返回該值;如果不存在,則返回默認(rèn)值空字符串。
package main
import (
    "fmt"
)
// 實(shí)現(xiàn)類似 Python 字典的 get() 方法
func get(myMap map[string]string, key string) string {
    value, ok := myMap[key]
    if !ok {
        return ""
    }
    return value
}
func main() {
    // 創(chuàng)建并初始化一個 map
    myMap := map[string]string{
        "apple":  "red",
        "banana": "yellow",
        "cherry": "red",
    }
    // 使用 get() 方法獲取鍵 "apple" 的值,如果不存在返回空字符串
    appleValue := get(myMap, "apple")
    fmt.Println("Color of 'apple':", appleValue)
    // 使用 get() 方法獲取鍵 "tangerine" 的值,如果不存在返回空字符串
    grapeValue := get(myMap, "tangerine")
    if grapeValue == "" {
        fmt.Println("沒有獲取到tangerine的對應(yīng)的值!")
    } else {
        fmt.Println("Color of 'tangerine':", grapeValue)
    }
}

運(yùn)行此代碼將輸出:

Color of 'apple': red
沒有獲取到tangerine的對應(yīng)的值!

7.5 使用delete()函數(shù)刪除鍵值對

使用delete()內(nèi)建函數(shù)從map中刪除一組鍵值對,delete()函數(shù)的格式如下:

delete(map, key)

其中:

  • map:表示要刪除鍵值對的map
  • key:表示要刪除的鍵值對的鍵

使用 delete 函數(shù)的情況下,傳入的第一個參數(shù)是我們的 map 類型變量,第二個參數(shù)就是我們想要刪除的鍵。我們可以看看這個代碼示例:

m := map[string]int {
  "key1" : 1,
  "key2" : 2,
}
fmt.Println(m) // map[key1:1 key2:2]
delete(m, "key2") // 刪除"key2"
fmt.Println(m) // map[key1:1]

7.6 遍歷 map 中的鍵值數(shù)據(jù)

最后,我們來說一下如何遍歷 map 中的鍵值數(shù)據(jù)。這一點(diǎn)雖然不像查詢和讀取操作那么常見,但日常開發(fā)中我們還是有這個需求的。在 Go 中,遍歷 map 的鍵值對只有一種方法,那就是像對待切片那樣通過 for range 語句對 map 數(shù)據(jù)進(jìn)行遍歷。我們看一個例子:

package main
import "fmt"
func main() {
    m := map[int]int{
        1: 11,
        2: 12,
        3: 13,
    }
    fmt.Printf("{ ")
    for k, v := range m {
        fmt.Printf("[%d, %d] ", k, v)
    }
    fmt.Printf("}\n")
}

你看,通過 for range 遍歷 map 變量 m,每次迭代都會返回一個鍵值對,其中鍵存在于變量 k 中,它對應(yīng)的值存儲在變量 v 中。我們可以運(yùn)行一下這段代碼,可以得到符合我們預(yù)期的結(jié)果:

{ [1, 11] [2, 12] [3, 13] }

如果我們只關(guān)心每次迭代的鍵,我們可以使用下面的方式對 map 進(jìn)行遍歷:

for k, _ := range m { 
  // 使用k
}

當(dāng)然更地道的方式是這樣的:

for k := range m {
  // 使用k
}

如果我們只關(guān)心每次迭代返回的鍵所對應(yīng)的 value,我們同樣可以通過空標(biāo)識符替代變量 k,就像下面這樣:

for _, v := range m {
  // 使用v
}

不過,前面 map 遍歷的輸出結(jié)果都非常理想,給我們的表象好像是迭代器按照 map 中元素的插入次序逐一遍歷。那事實(shí)是不是這樣呢?我們再來試試,多遍歷幾次這個 map 看看。

我們先來改造一下代碼:

package main
import "fmt"
func doIteration(m map[int]int) {
    fmt.Printf("{ ")
    for k, v := range m {
        fmt.Printf("[%d, %d] ", k, v)
    }
    fmt.Printf("}\n")
}
func main() {
    m := map[int]int{
        1: 11,
        2: 12,
        3: 13,
    }
    for i := 0; i < 3; i++ {
        doIteration(m)
    }
}

運(yùn)行一下上述代碼,我們可以得到這樣結(jié)果:

{ [1, 11] [2, 12] [3, 13] }
{ [2, 12] [3, 13] [1, 11] }
{ [1, 11] [2, 12] [3, 13] }

我們可以看到,對同一 map 做多次遍歷的時候,每次遍歷元素的次序都不相同。這是 Go 語言 map 類型的一個重要特點(diǎn),也是很容易讓 Go 初學(xué)者掉入坑中的一個地方。所以這里你一定要記?。?strong>程序邏輯千萬不要依賴遍歷 map 所得到的的元素次序。

八、Map的相等性

map 之間不能使用 == 操作符判斷,== 只能用來檢查 map 是否為 nil。

func main() {
    map1 := map[string]int{
        "one": 1,
        "two": 2,
    }
    map2 := map1
    if map1 ==nil{
        fmt.Println("map1為空")
    }else {
        fmt.Println("map1不為空")
    }
    if map1 == map2 { // 直接報錯,不能直接比較
    }
}

以上就是Go 復(fù)合類型之字典類型介紹的詳細(xì)內(nèi)容,更多關(guān)于Go 復(fù)合類型之字典類型介紹的資料請關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • Go語言中循環(huán)Loop的用法介紹

    Go語言中循環(huán)Loop的用法介紹

    這篇文章介紹了Go語言中循環(huán)Loop的用法,文中通過示例代碼介紹的非常詳細(xì)。對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下
    2022-07-07
  • 一文了解Go語言的并發(fā)特性

    一文了解Go語言的并發(fā)特性

    本文主要介紹了一文了解Go語言的并發(fā)特性,通過輕量級線程、通道及選擇語句,使得并發(fā)編程變得既簡單又高效,下面就來具體了解一下如何使用,感興趣的可以了解一下
    2024-02-02
  • Windows系統(tǒng)中搭建Go語言開發(fā)環(huán)境圖文詳解

    Windows系統(tǒng)中搭建Go語言開發(fā)環(huán)境圖文詳解

    GoLand?是?JetBrains?公司推出的商業(yè)?Go?語言集成開發(fā)環(huán)境(IDE),這篇文章主要介紹了Windows系統(tǒng)中搭建Go語言開發(fā)環(huán)境詳解,需要的朋友可以參考下
    2022-10-10
  • go語言實(shí)現(xiàn)二叉樹的序例化與反序列化

    go語言實(shí)現(xiàn)二叉樹的序例化與反序列化

    這篇文章主要介紹了go語言實(shí)現(xiàn)二叉樹的序例化與反序列化,文章圍繞主題展開詳細(xì)內(nèi)容介紹,具有一定的參考價值,需要的小伙伴可以參考一下
    2022-05-05
  • Golang 實(shí)現(xiàn)獲取當(dāng)前函數(shù)名稱和文件行號等操作

    Golang 實(shí)現(xiàn)獲取當(dāng)前函數(shù)名稱和文件行號等操作

    這篇文章主要介紹了Golang 實(shí)現(xiàn)獲取當(dāng)前函數(shù)名稱和文件行號等操作,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧
    2021-05-05
  • 詳解Go語言中select語句的常見用法

    詳解Go語言中select語句的常見用法

    這篇文章主要是來和大家介紹一下Go語言中select?語句的常見用法,以及在使用過程中的注意事項(xiàng),文中的示例代碼講解詳細(xì),感興趣的小伙伴可以了解一下
    2023-07-07
  • 使用go語言將單反斜杠改為雙反斜杠的方法

    使用go語言將單反斜杠改為雙反斜杠的方法

    最近開發(fā)的時候遇到這么個問題,就是在window上獲取了文件目錄的字段,然后將這個絕對路徑保存到數(shù)據(jù)庫,但是前端展示的時候路徑的雙反斜杠變成了單反斜杠,本文給大家介紹了使用go語言將單反斜杠改為雙反斜杠的方法,需要的朋友可以參考下
    2024-01-01
  • 詳解go語言 make(chan int, 1) 和 make (chan int) 的區(qū)別

    詳解go語言 make(chan int, 1) 和 make (chan int) 的區(qū)別

    這篇文章主要介紹了go語言 make(chan int, 1) 和 make (chan int) 的區(qū)別,本文給大家介紹的非常詳細(xì),具有一定的參考借鑒價值,需要的朋友可以參考下
    2020-01-01
  • golang如何替換換行符

    golang如何替換換行符

    這篇文章主要介紹了golang如何替換換行符問題,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教
    2024-07-07
  • 利用dep代替go get獲取私有庫的方法教程

    利用dep代替go get獲取私有庫的方法教程

    go get 從指定源上面下載或者更新指定的代碼和依賴,并對他們進(jìn)行編譯和安裝,但go get功能比較差,所以下面這篇文章主要給大家介紹了關(guān)于利用dep代替go get獲取私有庫的相關(guān)資料,需要的朋友可以參考借鑒,下面來一起看看吧。
    2017-11-11

最新評論