初探Golang數(shù)據結構之Slice的使用
在閱讀Go語言圣經時,一直對數(shù)組和切片的使用場景好奇,不明白為什么推薦使用切片來代替數(shù)組。希望能通過一些梳理,能更好的理解切片和數(shù)組,找到他們合適的使用場景。
切片與數(shù)組
關于切片和數(shù)組怎么選擇,我們來討論下這個問題。
在Go中,數(shù)組是值類型,賦值和函數(shù)傳參都會復制整個數(shù)組數(shù)據。
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)
}輸出結果:
a : 0xc0000180a0 , [100 200]
b : 0xc0000180b0 , [100 200]
array : 0xc0000180f0 , [100 200]
array : 0xc000018110 , [100 200]
可以看到,四個內存地址都不相同,印證了前面的說法。當數(shù)組數(shù)據量達到百萬級別時,復制數(shù)組會給內存帶來巨大的壓力,那能否通過傳遞指針來解決呢?
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
}輸出結果:
f1 array : 0xc0000b0008 , [100]
array : 0xc0000b0008 , [200]
可以看到,數(shù)組指針可以實現(xiàn)我們想要的效果,解決了復制數(shù)組帶來的內存問題,不過函數(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
}
//輸出結果
f1 array : 0xc000018098 , [100]
f2 array : 0xc00000c030 , [200]
array : 0xc000018098 , [300]可以看到,切片的指針和原來數(shù)組的指針是不同的。
總結
通常來說,使用數(shù)組進行參數(shù)傳遞會消耗較多內存,采用切片可以避免此問題。切片是引用傳遞,不會占用較多內存,效率更高一些。
切片的數(shù)據結構
切片在編譯期是 cmd/compile/internal/types/type.go 包下的Slice類型,而它的運行時的數(shù)據結構位于 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,最大為關聯(lián)數(shù)組的長度,切片是一個長度可變的動態(tài)窗口。
創(chuàng)建切片
使用make
slice := make([]int, 4, 6)
內存空間申請了6個int類型的內存大小。由于len=4,所以后面2個空間暫時無法訪問到,但是容量是存在的。此時數(shù)組里每個變量都=0。
字面量
slice := []int{0, 1, 2}nil切片和空切片
// nil 切片
var s []int
// 空切片
s2 := make([]int, 0)
s3 := []int{}空切片和 nil 切片的區(qū)別在于,空切片指向的地址不是nil,指向的是一個內存地址,但是它沒有分配任何內存空間,即底層元素包含0個元素。
func main() {
var s []int
s2 := []int{}
s3 := make([]int, 0)
fmt.Println(s == nil)
fmt.Println(s2 == nil)
fmt.Println(s3 == nil)
}
// 輸出結果
true
false
false簡單說,nil切片指針值為nil;而空切片的指針值不為nil,
需要說明的一點是,不管是使用 nil 切片還是空切片,對其調用內置函數(shù) append,len 和 cap 的效果都是一樣的。
但使用append時要注意:
- 如果append追加的數(shù)據長度小于等于cap-len,只做一次數(shù)據拷貝。
- 如果append追加的數(shù)據長度大于cap-len,則會分配一塊更大的內存,然后把原數(shù)據拷貝過來,再進行追加。
特別當我們需要構建一個切片,是從原有切片復制而來時,要注意值覆蓋問題。
func main() {
s1 := []int{0, 1, 2, 3} // 先定義一個現(xiàn)有切片
s2 := s1[0:1] // 復制現(xiàn)有的切片
s2 = append(s2, 100)
fmt.Print(s1)
}輸出結果
[0 100 2 3]
原因還是切片本質上是數(shù)組的一個動態(tài)窗口,當cap夠用時,不會新開辟內存空間進行復制,此時對數(shù)組的任何修改,都會對其他代理此數(shù)組的切片產生連帶影響。
到此這篇關于初探Golang數(shù)據結構之Slice的使用的文章就介紹到這了,更多相關Go Slice內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!

