初探Golang數(shù)據(jù)結(jié)構(gòu)之Slice的使用
在閱讀Go語言圣經(jīng)時,一直對數(shù)組和切片的使用場景好奇,不明白為什么推薦使用切片來代替數(shù)組。希望能通過一些梳理,能更好的理解切片和數(shù)組,找到他們合適的使用場景。
切片與數(shù)組
關(guān)于切片和數(shù)組怎么選擇,我們來討論下這個問題。
在Go中,數(shù)組是值類型,賦值和函數(shù)傳參都會復(fù)制整個數(shù)組數(shù)據(jù)。
func main() { a := [2]int{100, 200} // 賦值 var b = a fmt.Printf("a : %p , %v\n", &a, a) fmt.Printf("b : %p , %v\n", &b, b) // 函數(shù)傳參 f(a) f(b) } func f(array [2]int) { fmt.Printf("array : %p , %v\n", &array, array) }
輸出結(jié)果:
a : 0xc0000180a0 , [100 200]
b : 0xc0000180b0 , [100 200]
array : 0xc0000180f0 , [100 200]
array : 0xc000018110 , [100 200]
可以看到,四個內(nèi)存地址都不相同,印證了前面的說法。當數(shù)組數(shù)據(jù)量達到百萬級別時,復(fù)制數(shù)組會給內(nèi)存帶來巨大的壓力,那能否通過傳遞指針來解決呢?
func main() { a := [1]int{100} f1(&a) fmt.Printf("array : %p , %v\n", &a, a) } func f1(p *[1]int) { fmt.Printf("f1 array : %p , %v\n", p, *p) (*p)[0] += 100 }
輸出結(jié)果:
f1 array : 0xc0000b0008 , [100]
array : 0xc0000b0008 , [200]
可以看到,數(shù)組指針可以實現(xiàn)我們想要的效果,解決了復(fù)制數(shù)組帶來的內(nèi)存問題,不過函數(shù)接收的指針來自值拷貝,相對來說沒有切片那么靈活。
func main() { a := [1]int{100} f1(&a) // 切片 b := a[:] f2(&b) fmt.Printf("array : %p , %v\n", &a, a) } func f1(p *[1]int) { fmt.Printf("f1 array : %p , %v\n", p, *p) (*p)[0] += 100 } func f2(p *[]int) { fmt.Printf("f2 array : %p , %v\n", p, *p) (*p)[0] += 100 } //輸出結(jié)果 f1 array : 0xc000018098 , [100] f2 array : 0xc00000c030 , [200] array : 0xc000018098 , [300]
可以看到,切片的指針和原來數(shù)組的指針是不同的。
總結(jié)
通常來說,使用數(shù)組進行參數(shù)傳遞會消耗較多內(nèi)存,采用切片可以避免此問題。切片是引用傳遞,不會占用較多內(nèi)存,效率更高一些。
切片的數(shù)據(jù)結(jié)構(gòu)
切片在編譯期是 cmd/compile/internal/types/type.go 包下的Slice類型,而它的運行時的數(shù)據(jù)結(jié)構(gòu)位于 reflect.SliceHeader
type SliceHeader struct { Data uintptr // 指向數(shù)組的指針 Len int // 當前切片的長度 Cap int // 當前切片的容量,cap 總是 >= len } // 占用24個字節(jié) fmt.Println(unsafe.Sizeof(reflect.SliceHeader{}))
切片是對數(shù)組一個連續(xù)片段的引用,這個片段可以是整個數(shù)組,也可以是數(shù)組的一部分。切片的長度可以在運行時修改,最小為0,最大為關(guān)聯(lián)數(shù)組的長度,切片是一個長度可變的動態(tài)窗口。
創(chuàng)建切片
使用make
slice := make([]int, 4, 6)
內(nèi)存空間申請了6個int類型的內(nèi)存大小。由于len=4,所以后面2個空間暫時無法訪問到,但是容量是存在的。此時數(shù)組里每個變量都=0。
字面量
slice := []int{0, 1, 2}
nil切片和空切片
// nil 切片 var s []int // 空切片 s2 := make([]int, 0) s3 := []int{}
空切片和 nil 切片的區(qū)別在于,空切片指向的地址不是nil,指向的是一個內(nèi)存地址,但是它沒有分配任何內(nèi)存空間,即底層元素包含0個元素。
func main() { var s []int s2 := []int{} s3 := make([]int, 0) fmt.Println(s == nil) fmt.Println(s2 == nil) fmt.Println(s3 == nil) } // 輸出結(jié)果 true false false
簡單說,nil切片指針值為nil;而空切片的指針值不為nil,
需要說明的一點是,不管是使用 nil 切片還是空切片,對其調(diào)用內(nèi)置函數(shù) append,len 和 cap 的效果都是一樣的。
但使用append時要注意:
- 如果append追加的數(shù)據(jù)長度小于等于cap-len,只做一次數(shù)據(jù)拷貝。
- 如果append追加的數(shù)據(jù)長度大于cap-len,則會分配一塊更大的內(nèi)存,然后把原數(shù)據(jù)拷貝過來,再進行追加。
特別當我們需要構(gòu)建一個切片,是從原有切片復(fù)制而來時,要注意值覆蓋問題。
func main() { s1 := []int{0, 1, 2, 3} // 先定義一個現(xiàn)有切片 s2 := s1[0:1] // 復(fù)制現(xiàn)有的切片 s2 = append(s2, 100) fmt.Print(s1) }
輸出結(jié)果
[0 100 2 3]
原因還是切片本質(zhì)上是數(shù)組的一個動態(tài)窗口,當cap夠用時,不會新開辟內(nèi)存空間進行復(fù)制,此時對數(shù)組的任何修改,都會對其他代理此數(shù)組的切片產(chǎn)生連帶影響。
到此這篇關(guān)于初探Golang數(shù)據(jù)結(jié)構(gòu)之Slice的使用的文章就介紹到這了,更多相關(guān)Go Slice內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
go 類型轉(zhuǎn)換方式(interface 類型的轉(zhuǎn)換)
這篇文章主要介紹了go 類型轉(zhuǎn)換方式(interface 類型的轉(zhuǎn)換),具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2021-05-05Go文件操作(新建打開寫入讀取刪除關(guān)閉)學(xué)習(xí)筆記
這篇文章主要為大家介紹了Go文件操作(新建打開寫入讀取刪除關(guān)閉)學(xué)習(xí)筆記,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2024-01-01go語言實現(xiàn)并發(fā)網(wǎng)絡(luò)爬蟲的示例代碼
本文主要介紹了go語言實現(xiàn)并發(fā)網(wǎng)絡(luò)爬蟲的示例代碼,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-03-03