您好,登錄后才能下訂單哦!
slice介紹
數(shù)組的長度在定義之后無法再次修改;數(shù)組是值類型,每次傳遞都將產(chǎn)生一份副本。顯然這種數(shù)據(jù)結(jié)構(gòu)無法完全滿足開發(fā)者的真實需求。在初始定義數(shù)組時,我們并不知道需要多大的數(shù)組,因此我們就需要“動態(tài)數(shù)組”。在Go里面這種數(shù)據(jù)結(jié)構(gòu)叫slice,slice并不是真正意義上的動態(tài)數(shù)組,而是一個引用類型。slice總是指向一個底層array,slice的聲明也可以像array一樣,只是不需要長度,它是可變長的,可以隨時往slice里面加數(shù)據(jù)。
初看起來,數(shù)組切片就像一個指向數(shù)組的指針,實際上它擁有自己的數(shù)據(jù)結(jié)構(gòu),而不僅僅是個指針。數(shù)組切片的數(shù)據(jù)結(jié)構(gòu)可以抽象為以下3個變量:
1.一個指向原生數(shù)組的指針(point):指向數(shù)組中slice指定的開始位置;
2.數(shù)組切片中的元素個數(shù)(len):即slice的長度;
3.數(shù)組切片已分配的存儲空間(cap):也就是slice開始位置到數(shù)組的最后位置的長度。
從底層實現(xiàn)的角度來看,數(shù)組切片實際上仍然使用數(shù)組來管理元素,基于數(shù)組,數(shù)組切片添加了一系列管理功能,可以隨時動態(tài)擴充存放空間,并且可以被隨意傳遞而不會導致所管理的元素被重復復制。
slice聲明
聲明slice時方括號[]內(nèi)沒有任何數(shù)據(jù)
聲明一個元素類型為int的slice
var mySlice []int 聲明兩個元素類型為byte的slice
golang 中的 slice 非常強大,讓數(shù)組操作非常方便高效。在開發(fā)中不定長度表示的數(shù)組全部都是 slice 。但是很多同學對 slice 的模糊認識,造成認為golang中的數(shù)組是引用類型,結(jié)果就是在實際開發(fā)中碰到很多坑,以至于出現(xiàn)一些莫名奇妙的問題,數(shù)組中的數(shù)據(jù)丟失了。
下面我們就開始詳細理解下 slice ,理解后會對開發(fā)出高效的程序非常有幫助。
這個是 slice 的數(shù)據(jù)結(jié)構(gòu),它很簡單,一個指向真實 array 地址的指針 ptr ,slice 的長度 len 和容量 cap 。
其中 len 和 cap 就是我們在調(diào)用 len(slice) 和 cap(slice) 返回的值。
我們來按照 slice 的數(shù)據(jù)結(jié)構(gòu)定義來解析出 ptr, len, cap
// 按照上圖定義的數(shù)據(jù)結(jié)構(gòu) type Slice struct { ptr unsafe.Pointer // Array pointer len int // slice length cap int // slice capacity }
下面寫一個完整的程序,嘗試把golang中slice的內(nèi)存區(qū)域轉(zhuǎn)換成我們定義的 Slice 進行解析
package main import ( "fmt" "unsafe" ) // 按照上圖定義的數(shù)據(jù)結(jié)構(gòu) type Slice struct { ptr unsafe.Pointer // Array pointer len int // slice length cap int // slice capacity } // 因為需要指針計算,所以需要獲取int的長度 // 32位 int length = 4 // 64位 int length = 8 var intLen = int(unsafe.Sizeof(int(0))) func main() { s := make([]int, 10, 20) // 利用指針讀取 slice memory 的數(shù)據(jù) if intLen == 4 { // 32位 m := *(*[4 + 4*2]byte)(unsafe.Pointer(&s)) fmt.Println("slice memory:", m) } else { // 64 位 m := *(*[8 + 8*2]byte)(unsafe.Pointer(&s)) fmt.Println("slice memory:", m) } // 把slice轉(zhuǎn)換成自定義的 Slice struct slice := (*Slice)(unsafe.Pointer(&s)) fmt.Println("slice struct:", slice) fmt.Printf("ptr:%v len:%v cap:%v \n", slice.ptr, slice.len, slice.cap) fmt.Printf("golang slice len:%v cap:%v \n", len(s), cap(s)) s[0] = 0 s[1] = 1 s[2] = 2 // 轉(zhuǎn)成數(shù)組輸出 arr := *(*[3]int)(unsafe.Pointer(slice.ptr)) fmt.Println("array values:", arr) // 修改 slice 的 len slice.len = 15 fmt.Println("Slice len: ", slice.len) fmt.Println("golang slice len: ", len(s)) }
運行一下查看結(jié)果
$ go run slice.go slice memory: [0 64 6 32 200 0 0 0 10 0 0 0 0 0 0 0 20 0 0 0 0 0 0 0] slice struct: &{0xc820064000 10 20} ptr:0xc820064000 len:10 cap:20 golang slice len:10 cap:20 array values: [0 1 2] Slice len: 15 golang slice len: 15
看到了,golang slice 的memory內(nèi)容,和自定義的 Slice 的值,還有按照 slice 中的指針指向的內(nèi)存,就是實際 Array 數(shù)據(jù)。當修改了 slice 中的len, len(s) 也變了。
接下來結(jié)合幾個例子,了解下slice一些用法
聲明一個Array通常使用 make ,可以傳入2個參數(shù),也可傳入3個參數(shù),第一個是數(shù)據(jù)類型,第二個是 len ,第三個是 cap 。如果不穿入第三個參數(shù),則 cap=len ,append 可以用來向數(shù)組末尾追加數(shù)據(jù)。
這是一個 append 的測試
// 每次cap改變,指向array的ptr就會變化一次 s := make([]int, 1) fmt.Printf("len:%d cap: %d array ptr: %v \n", len(s), cap(s), *(*unsafe.Pointer)(unsafe.Pointer(&s))) for i := 0; i < 5; i++ { s = append(s, i) fmt.Printf("len:%d cap: %d array ptr: %v \n", len(s), cap(s), *(*unsafe.Pointer)(unsafe.Pointer(&s))) } fmt.Println("Array:", s)
運行結(jié)果
len:1 cap: 1 array ptr: 0xc8200640f0 len:2 cap: 2 array ptr: 0xc820064110 len:3 cap: 4 array ptr: 0xc8200680c0 len:4 cap: 4 array ptr: 0xc8200680c0 len:5 cap: 8 array ptr: 0xc82006c080 len:6 cap: 8 array ptr: 0xc82006c080 Array: [0 0 1 2 3 4]
看出來了吧,每次cap改變的時候指向array內(nèi)存的指針都在變化。當在使用 append 的時候,如果 cap==len 了這個時候就會新開辟一塊更大內(nèi)存,然后把之前的數(shù)據(jù)復制過去。
實際go在append的時候放大cap是有規(guī)律的。在 cap 小于1024的情況下是每次擴大到 2 * cap ,當大于1024之后就每次擴大到 1.25 * cap 。所以上面的測試中cap變化是 1, 2, 4, 8
在實際使用中,我們最好事先預期好一個cap,這樣在使用append的時候可以避免反復重新分配內(nèi)存復制之前的數(shù)據(jù),減少不必要的性能消耗。
創(chuàng)建切片
s := []int{1, 2, 3, 4, 5} fmt.Printf("len:%d cap: %d array ptr: %v \n", len(s), cap(s), *(*unsafe.Pointer)(unsafe.Pointer(&s))) fmt.Println("Array:", s) s1 := s[1:3] fmt.Printf("len:%d cap: %d array ptr: %v \n", len(s1), cap(s1), *(*unsafe.Pointer)(unsafe.Pointer(&s1))) fmt.Println("Array", s1)
運行結(jié)果
len:5 cap: 5 array ptr: 0xc820012210 Array: [1 2 3 4 5] len:2 cap: 4 array ptr: 0xc820012218 Array [2 3]
在一個切片基礎上創(chuàng)建新的切片 s1 ,新切片的 ptr 指向的就是 s1[0] 數(shù)據(jù)的內(nèi)存地址??梢钥吹街羔樀刂?0xc820012210 與 0xc820012218 相差 8byte 正好是一個int類型長度,cap也相應的變?yōu)?
就寫到這里了,總結(jié)一下,切片的結(jié)構(gòu)是指向數(shù)據(jù)的指針,長度和容量。復制切片,或者在切片上創(chuàng)建新切片,切片中的指針都指向相同的數(shù)據(jù)內(nèi)存區(qū)域。
知道了切片原理就可以在開發(fā)中避免出現(xiàn)錯誤了,希望這篇博客可以給大家?guī)韼椭R蚕M蠹叶喽嘀С謨|速云。
免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。