golang slice map 的一些常見坑
1,常看到有部落格或者書說道,golang內部有引用型別,這裡說明一哈,golang裡面所有的型別都是值型別,之所以有些用起來像引用,是在於該型別內部是用指標實現的,但是其本質就是包含指標的結構體。
我們常常用錯的型別就是Slice.以下是slice這個型別的實現:
type slice struct { array unsafe.Pointer // 內部是分配一個連續的記憶體塊,這個指標就指向這個記憶體的首地址 len int // 陣列長度 cap int // 陣列容量 } 當不斷的向slice新增資料,len就不斷變大,當len > cap的時候,內部開始擴容,重新分配一塊記憶體,大致的擴容策略是cap * 2,然後將老地址的資料copy到新地址。 由於以上的特性,當有一個slice a, 將a 複製給b, 這個時候,a, b指向同一個底層陣列,當對a不斷的進行新增操作,當a進行擴容後,a,b便開始指向不同的底層陣列。所有在操作slice的時候要格外注意。
我們常常的錯誤使用方式有以下幾點:
// 錯誤使用方式1: data := make(map[string][]int32) item0 := make([]int32, 0) item0 = append(item0, 1) item0 = append(item0, 2) data["item0"] = item0 // 獲取到item0, 並向其中新增兩項 temp := data["item0"] temp = append(temp, 3) temp = append(temp, 4) // 輸出結果是:temp len is 4, data["item0"] len is 2 // 注意,這裡返回的temp 是一個副本,雖然內部指向同一個底層陣列,但是仍然是不同的slice值 fmt.Printf("temp len is %v, data[\"item0\"] len is %v\n", len(temp), len(data["item0"])) // 錯誤使用方式2: item1 := make([]int32, 0, 2) item1 = append(item1, 1) item1 = append(item1, 2) // 注意,這裡的賦值,只是簡單的複製了一個slice的副本,item1, item2擁有相同的cap, len, // 以及指向同一個底層陣列的指標,在沒有擴容前,因為共享同一個底層陣列,就需要格外小心 item2 := item1 // item1擴容前,item1, item2的修改會相互影響 // 結果:item1[0] is 6, item2[0] is 6 item2[0] = 6 fmt.Printf("item1[0] is %v, item2[0] is %v\n", item1[0], item2[0]) // item2擴容後,item1, item2分別擁有不同的底層陣列 // 結果:item1[0] is 6, item2[0] is 4 item2 = append(item2, 3) item2[0] = 4 fmt.Printf("item1[0] is %v, item2[0] is %v\n", item1[0], item2[0])
2,然後說一說golang裡面的map,golang裡面的是通過hashtable來實現的,具體方式就是通過拉鍊法(陣列+連結串列)來實現的,這裡對比一哈c++的map,c++裡面的map 是通過紅黑樹來實現的,所以二者在遍歷的時候做刪除操作,golang的是可以直接操作的,因為內部實現是雜湊對映,刪除並不影響其他項,而c++中的map刪除,由於是紅黑樹,刪除任意一項,都會打亂迭代指標,所以不能直接刪除。同時,golang裡面的key是無序的,即使你順序新增,遍歷的時候也是無序。
golang裡面的map,當通過key獲取到value時,這個value是不可定址的,因為map 會進行動態擴容,當進行擴充套件後,map的value就會進行記憶體遷移,其地址發生變化,所以無法對這個value進行定址。
所以就產生了如下錯誤:
type Entity struct {
Desc string
}
entityMap := make(map[string]Entity, 0)
entityMap["Cup"] = Entity{Desc: "This is a Cup"}
// 這裡會編譯報錯,因為entityMap["Cup"]是不可定址的,所以不能直接訪問內部變數
entityMap["Cup"].Desc = "This is a special Cup"
3,看一看map 的內部實現,map 內部只要是兩個資料結構,hmap和bmap,hmap裡面最主要的是一個buckets指標,指向一個bucket陣列, 而每個bucket(bmap)分別放著tophash陣列, key/value鍵值對(key1, key2, key3...|value1,value2,value3...),next bucket指標。儲存鍵值對,將key, value分別放在一起,是為了記憶體對齊,減少記憶體消耗。
對於擴容,當裝載因子過小時,會觸發擴容,一般情況是以2倍的方式分配一個新的bucket陣列,然後將oldbucket的資料複製到newbucket, 但是不是立即複製,而是當再次訪問oldbucket裡面的資料時,才將對應的bucket複製到newbuckets的桶裡,copy完成後,oldbucket被gc回收。
當對map 進行delete操作時,並不會刪除對於的記憶體空間,只是將bucket裡面的對應項清空。
4,對map而言,make返回的是指標,所有a:=b,其實就是建立指標副本,所以map在進行賦值,其實是hmap指標的複製,所以賦值後的變數共享同一個hamp,所以當任意一個map變數擴容,其他的變數都相應發生變化,表現特別像引用
// makemap implements Go map creation for make(map[k]v, hint).
// If the compiler has determined that the map or the first bucket
// can be created on the stack, h and/or bucket may be non-nil.
// If h != nil, the map can be created directly in h.
// If h.buckets != nil, bucket pointed to can be used as the first bucket.
func makemap(t *maptype, hint int, h *hmap) *hmap {
}
所以我們在使用的時候要注意:
// 返回一個hmap 指標,
a := make(map[int]int, 2)
a[1] = 1
a[2] = 2
// 建立一個*hamp副本, a, b共享一個hmap實體
b := a
對slice而言,make返回的是物件,所以a:=b,就是建立一個物件副本。ab是不同的slice實體,剛開始,ab指向同一個底層陣列,當其中一個變數擴容後,ab開始指向不同的底層陣列
func makeslice(et *_type, len, cap int) slice {
// NOTE: The len > maxElements check here is not strictly necessary,
// but it produces a 'len out of range' error instead of a 'cap out of range' error
// when someone does make([]T, bignumber). 'cap out of range' is true too,
// but since the cap is only being supplied implicitly, saying len is clearer.
// See issue 4085.
}
//對slice而言,make返回的是物件,所以a:=b,就是建立一個物件副本
a := make([]int, 2)
a = append(e, 1)
a = append(e, 2)
// 建立一個slice副本, a, b是兩個不同的slice實體,內部指標指向同一個底層陣列
b := a