GO中的slice使用簡介(源碼分析slice)
slice表示切片(分片),例如對一個數組進行切片,取出數組中的一部分值。在現(xiàn)代編程語言中,slice(切片)幾乎成為一種必備特性,它可以從一個數組(列表)中取出任意長度的子數組(列表),為操作數據結構帶來非常大的便利性,如python、perl等都支持對數組的slice操作,甚至perl還支持對hash數據結構的slice。
但Go中的slice和這些語言的slice不太一樣,前面所說的語言中,slice是一種切片的操作,切片后返回一個新的數據對象。而Go中的slice不僅僅是一種切片動作,還是一種數據結構(就像數組一樣)。
GO-slice詳解
簡介
slice(切片)是go中常見和強大的類型,這篇文章不是slice使用簡介,從源碼角度來分析slice的實現(xiàn),slice的一些迷惑的使用方式,同時也講清楚一些問題。
slice的底層實現(xiàn)是數組,是對數組的抽象
官方有slice相關的博客
https://go.dev/blog/slices-introhttps://go.dev/blog/slices
slice分析
數據結構
源碼:https://github.com/golang/go/blob/master/src/runtime/slice.go#L14
slice結構如下:
type slice struct { array unsafe.Pointer // 數組指針 len int //切片長度 cap int // 切片容量 }
array:為是底層數組的指針
len:切片中已有元素的個數
cap:底層數組的長度
原理概述:
切片的底層實現(xiàn)是數組,len是切片的個數,cap是底層數組的長度,當往切片中追加元素的時候,len++,如果len>cap,就會觸發(fā)切片擴容,擴容邏輯是算一個新的cap,并且創(chuàng)建一個新的底層數組,將原來的數據copy過來。并且創(chuàng)建一個新的切片(slice)。
可以從一個切片中創(chuàng)建一個新的切片,底層數組是同用的,修改切片元素的時候會影響到新的切片。
如果所示:
var s = make([]byte,5)
對應的邏輯是,創(chuàng)建一個長度和容量為5的數組,如果所示
切片的創(chuàng)建方式
先看切片的創(chuàng)建方式,說這個問題之前,先看看切片的創(chuàng)建方式
聲明
var vocabList []uint64
聲明了一個[]uint64類型的切片,vocabList為切片的0值,
這得說一下go中nil
值
在 Go 中,nil 是指針、接口、映射、切片、通道和函數類型的零值,表示未初始化的值。
具體的可以看:https://go101.org/article/nil.html
回到代碼,這表示nil值,它的len和cap都是0,和nil比較結果為true,這里要說,nil值對應的具體的類型是在上下文中編譯器推導出來的
package main func main() { var vocabList []uint64 println(vocabList == nil) } // output: // true
通過new創(chuàng)建
var vocabList = *new([]uint64)
new是內建函數,用來分配指定類型的內容,返回指向內存地址的指針,并且給此地址分配此類型的0值。
字面量創(chuàng)建
var vocabList = []uint64{1,2,3,4}
make
var vocabList = make([]uint64,10)
make接受三個參數,在創(chuàng)建的時候指定類型,長度,容量,不指定容量,默認和長度一樣
代碼如下:
func main() { var vocabList = make([]uint64,10) fmt.Printf("slice:%v,len:%d,cap:%d",vocabList,len(vocabList),cap(vocabList)) } // output: // slice:[0 0 0 0 0 0 0 0 0 0],len:10,cap:10
從切片或數組截取
var vocabList = resList[3:5]
兩個切片公用一個底層數組,但如果新創(chuàng)建的切片擴容了,就不共用了。
問題分析
主要分析幾個問題
nil切片和空切片的差異
nil切片是通過 new 和聲明方式創(chuàng)建的切片,go會給他們nil值,如下面的代碼所示:
var vocabList []int vocabList == nil //true
空切片是通過make,字面量方式創(chuàng)建的長度為0的切片,
vocabList := make([]int,0) vocabList == nil // false vocabList1 = []int{} vocabList1 == nil //false
具體可以看這篇文章:http://www.dbjr.com.cn/jiaoben/288490px1.htm
我下面的代碼和內容來于這篇文章
通過unsafe.Pointer
可以將對應地址中的數據轉換為任何符合go中類型的變量
可以看到,空切片的是有底層數組的,并且底層數組都一樣,其實也可以說空切片執(zhí)行了一個指定的地址空間,
這個地址空間在源碼中有定義,當分配的大小為0的時候會返回這個地址,要說明的是這個地址空間不是固定的,不是寫死的一個數,在不同的機器上運行會有不同的值。
源碼:https://github.com/golang/go/blob/master/src/runtime/malloc.go#L948
兩者的差異:
嵌套在結構體中不容易發(fā)現(xiàn)
package main type Word struct { SenseIds []int } func main() { word := Word{} println(word.SenseIds == nil) //true word1 := Word{ SenseIds: make([]int,0), } println(word1.SenseIds == nil) //false }
json序列化
package main import "encoding/json" type Word struct { SenseIds []int `json:"sense_ids" ` } func main() { word := Word{} marshal, err := json.Marshal(word) if err != nil { return } println(string(marshal)) //{"sense_ids":null} word1 := Word{ SenseIds: []int{}, } marshal1, err := json.Marshal(word1) if err != nil { return } println(string(marshal1)) //{"sense_ids":[]} }
這個問題我深有體會
在做一個需求的時候,看到編輯器報黃色提示,提示我將 var a = []int{}
改為var a []int
,當然,go官方也是這么建議的。我就改了,然后一個接口就報錯了。給我一頓找,發(fā)現(xiàn)json返回了null。
除此之外,沒有別的區(qū)別。
切片共用底層數組
在做截取的時候,會創(chuàng)建一個新的slice,截取語法如下
bSlice := aSlice[start:stop:capacityIndex] // satrt <= stop <= capacityIndex //capacityIndex不是必須的,默認=原來切片的cap-startIndex // 如果指定 新切片的容量為 capacityIndex-start
如圖所示:
有了上面的例子,可以看如下代碼
package main import ( "fmt" ) func main() { slice := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9} s1 := slice[2:5] fmt.Println("============= 1 ==============") fmt.Printf("%v len:%d cap:%d \n",slice,len(slice),cap(slice)) fmt.Printf("%v len:%d cap:%d \n",s1,len(s1),cap(s1)) s2 := s1[2:6:7] fmt.Println("============= 2 ==============") fmt.Printf("%v len:%d cap:%d \n",slice,len(slice),cap(slice)) fmt.Printf("%v len:%d cap:%d \n",s1,len(s1),cap(s1)) fmt.Printf("%v len:%d cap:%d \n",s2,len(s2),cap(s2)) // 到這里是正確的切片操作,slice,s1,s2通用底層數組 s2 = append(s2, 100) //s2追加100,此時s2中l(wèi)en=cap,還沒有觸發(fā)擴容操作 fmt.Println("============= 3 ==============") fmt.Printf("%v len:%d cap:%d \n",slice,len(slice),cap(slice)) fmt.Printf("%v len:%d cap:%d \n",s1,len(s1),cap(s1)) fmt.Printf("%v len:%d cap:%d \n",s2,len(s2),cap(s2)) s2 = append(s2, 200)// 200加不進去了,觸發(fā)擴容操作,此時s2的底層數組和s1,slice不一樣了 fmt.Println("============= 3 ==============") fmt.Printf("%v len:%d cap:%d \n",slice,len(slice),cap(slice)) fmt.Printf("%v len:%d cap:%d \n",s1,len(s1),cap(s1)) fmt.Printf("%v len:%d cap:%d \n",s2,len(s2),cap(s2)) s1[2] = 20 // s1和slice底層數組還是一樣的 fmt.Println("============= 3 ==============") fmt.Printf("%v len:%d cap:%d \n",slice,len(slice),cap(slice)) fmt.Printf("%v len:%d cap:%d \n",s1,len(s1),cap(s1)) fmt.Printf("%v len:%d cap:%d \n",s2,len(s2),cap(s2)) } 輸出如下: ============= 1 ============== [0 1 2 3 4 5 6 7 8 9] len:10 cap:10 [2 3 4] len:3 cap:8 ============= 2 ============== [0 1 2 3 4 5 6 7 8 9] len:10 cap:10 [2 3 4] len:3 cap:8 [4 5 6 7] len:4 cap:5 ============= 3 ============== [0 1 2 3 4 5 6 7 100 9] len:10 cap:10 [2 3 4] len:3 cap:8 [4 5 6 7 100] len:5 cap:5 ============= 3 ============== [0 1 2 3 4 5 6 7 100 9] len:10 cap:10 [2 3 4] len:3 cap:8 [4 5 6 7 100 200] len:6 cap:10 ============= 3 ============== [0 1 2 3 20 5 6 7 100 9] len:10 cap:10 [2 3 20] len:3 cap:8 [4 5 6 7 100 200] len:6 cap:10
源碼分析
make創(chuàng)建切片
使用dlv或者go提供的匯編工具可以看到 make調用了什么函數
源碼:https://github.com/golang/go/blob/master/src/runtime/slice.go#LL88C18-L88C18
切片的擴容規(guī)則
版本不同,擴容規(guī)則可能不一樣
,例子中go版本為:
代碼如下:
package main import ( "fmt" "unsafe" ) func main() { ints := make([]int, 0) // 創(chuàng)建了一個長度為0的切片 i := *(*[3]int)(unsafe.Pointer(&ints)) // 利用Pointer將slice轉換為長度為3的int數組,此操作可以查看slice結構體中各個字段的數值 fmt.Printf("slice1:%v \n",i) fmt.Printf("slice:%v \n",ints) var ints1 = append(ints, 1) // 追加一個元素 i2 := *(*[3]int)(unsafe.Pointer(&ints1)) fmt.Printf("slice2 %v \n",i2) // slice對應的底層數組發(fā)生了擴容操作,底層數組已經變了 fmt.Printf("slice2:%v \n",ints) }
用dlv 查看它的匯編代碼,看擴容操作調用了那些函數
源碼鏈接:https://github.com/golang/go/blob/master/src/runtime/slice.go#LL157C10-L157C10
// 函數入參說明如下 //1. et 類型 //2. old 老切片 //3. cap 需要分配的指定容量,為了方便期間,調用這個函數的時候cap傳遞的都是老的slice的cap func growslice(et *_type, old slice, cap int) slice { if raceenabled { // 是否啟動競爭檢測 callerpc := getcallerpc() racereadrangepc(old.array, uintptr(old.len*int(et.size)), callerpc, abi.FuncPCABIInternal(growslice)) } if msanenabled { //內存檢查,確保沒有未初始化的內存被使用 msanread(old.array, uintptr(old.len*int(et.size))) } if asanenabled { // 檢查內存訪問是否越界 asanread(old.array, uintptr(old.len*int(et.size))) } if cap < old.cap { panic(errorString("growslice: cap out of range")) } if et.size == 0 { // 正常是不會這樣的,但為了安全還是處理了0的情況 return slice{unsafe.Pointer(&zerobase), old.len, cap} } // 開始計算新的cap newcap := old.cap doublecap := newcap + newcap // 2倍 if cap > doublecap { // 新的cap要是老的2倍 newcap = cap } else { const threshold = 256 if old.cap < threshold { // cap小于256,newCap為oldCap的兩倍 newcap = doublecap } else { for 0 < newcap && newcap < cap { // Transition from growing 2x for small slices // to growing 1.25x for large slices. This formula // gives a smooth-ish transition between the two. // 這個公式可以當超過256之后i,可以實現(xiàn)1.25到2倍的平滑過渡 newcap += (newcap + 3*threshold) / 4 // 這個公式化簡一下 newCap = oldCap*1.25 + 192(3/4*256) } // Set newcap to the requested cap when // the newcap calculation overflowed. if newcap <= 0 { // 防止溢出 newcap = cap } } } // 下面的邏輯是對上面計算出來的newCap來做對齊操作,上面的計算不是真正的結果,下面還需要做內存對齊操作。 var overflow bool var lenmem, newlenmem, capmem uintptr switch { case et.size == 1: lenmem = uintptr(old.len) newlenmem = uintptr(cap) capmem = roundupsize(uintptr(newcap)) overflow = uintptr(newcap) > maxAlloc newcap = int(capmem) case et.size == goarch.PtrSize: lenmem = uintptr(old.len) * goarch.PtrSize newlenmem = uintptr(cap) * goarch.PtrSize capmem = roundupsize(uintptr(newcap) * goarch.PtrSize) overflow = uintptr(newcap) > maxAlloc/goarch.PtrSize newcap = int(capmem / goarch.PtrSize) case isPowerOfTwo(et.size): var shift uintptr if goarch.PtrSize == 8 { // Mask shift for better code generation. shift = uintptr(sys.Ctz64(uint64(et.size))) & 63 } else { shift = uintptr(sys.Ctz32(uint32(et.size))) & 31 } lenmem = uintptr(old.len) << shift newlenmem = uintptr(cap) << shift capmem = roundupsize(uintptr(newcap) << shift) overflow = uintptr(newcap) > (maxAlloc >> shift) newcap = int(capmem >> shift) default: lenmem = uintptr(old.len) * et.size newlenmem = uintptr(cap) * et.size capmem, overflow = math.MulUintptr(et.size, uintptr(newcap)) capmem = roundupsize(capmem) newcap = int(capmem / et.size) } // The check of overflow in addition to capmem > maxAlloc is needed // to prevent an overflow which can be used to trigger a segfault // on 32bit architectures with this example program: // // type T [1<<27 + 1]int64 // // var d T // var s []T // // func main() { // s = append(s, d, d, d, d) // print(len(s), "\n") // } if overflow || capmem > maxAlloc { panic(errorString("growslice: cap out of range")) } var p unsafe.Pointer // 下面是分配新的數組 if et.ptrdata == 0 { // 原slice底層數組為0,也就是nil切片, p = mallocgc(capmem, nil, false) // The append() that calls growslice is going to overwrite from old.len to cap (which will be the new length). // Only clear the part that will not be overwritten. memclrNoHeapPointers(add(p, newlenmem), capmem-newlenmem) // 然后使用memclrNoHeapPointers函數來清除新分配的內存 } else { // Note: can't use rawmem (which avoids zeroing of memory), because then GC can scan uninitialized memory. p = mallocgc(capmem, et, true) // 分配新的底層數組 if lenmem > 0 && writeBarrier.enabled { // 之前有數據,并且寫屏障已經開啟 // Only shade the pointers in old.array since we know the destination slice p // only contains nil pointers because it has been cleared during alloc. bulkBarrierPreWriteSrcOnly(uintptr(p), uintptr(old.array), lenmem-et.size+et.ptrdata) } } // copy元素 memmove(p, old.array, lenmem) // 創(chuàng)建新的切片返回 return slice{p, old.len, newcap} }
總結如下:
- 確定新容量,cap小于256,直接2倍,大于256,新容量=老容量*1.25 * 3/4 * 256
- 用新容量來做內存對齊操作
- 分配新數組
- copy數組
- 創(chuàng)建切片返回
還有一點:
append的操作匯編并沒有調用函數,在匯編層面就做了,直接往底層數組添加元素,只有數組已經滿的情況下才會觸發(fā)擴容操作
內存對齊相關東西之后在說
我們來一個例子來驗證一下上面的代碼邏輯:
package main import "fmt" func main() { var s = []int{} oldCap := cap(s) for i := 0; i < 2048; i++ { s = append(s, i) newCap := cap(s) if newCap != oldCap { // 追加元素,當容量發(fā)生變化的時候,打印,擴容之前的元素,cap,導致擴容的元素,和擴容之后的cap fmt.Printf("[%d -> %4d] cap = %-4d after append %-4d cap = %-4d\n", 0, i-1, oldCap, i, newCap) oldCap = newCap } } } // outPut [0 -> -1] cap = 0 | after append 0 cap = 1 [0 -> 0] cap = 1 | after append 1 cap = 2 [0 -> 1] cap = 2 | after append 2 cap = 4 [0 -> 3] cap = 4 | after append 4 cap = 8 [0 -> 7] cap = 8 | after append 8 cap = 16 [0 -> 15] cap = 16 | after append 16 cap = 32 [0 -> 31] cap = 32 | after append 32 cap = 64 [0 -> 63] cap = 64 | after append 64 cap = 128 [0 -> 127] cap = 128 | after append 128 cap = 256 // 256之前都是2倍 [0 -> 255] cap = 256 | after append 256 cap = 512 // 1.25 * 256 + 192 = 512 [0 -> 511] cap = 512 | after append 512 cap = 848 // 1.25 * 512 + 192 = 832 [0 -> 847] cap = 848 | after append 848 cap = 1280 [0 -> 1279] cap = 1280 | after append 1280 cap = 1792 [0 -> 1791] cap = 1792 | after append 1792 cap = 2560
copy函數的使用
copy函數底層調用的是
底層數組共用,那copy函數就可以完成一下幾種操作
移動slice中的元素
func main() { var vocabs = []int{1,2,3,4,5,6,7,8,9} // 現(xiàn)在將5去除掉,將5之后的移動到前面 copy(vocabs[4:],vocabs[5:]) fmt.Printf("%v",vocabs) } //outPut [1 2 3 4 6 7 8 9 9]
slice合并
func main() { ints := make([]int, 10) i1 := make([]int,0, 5) for i := 0; i < 5; i++ { i1 = append(i1, i) } i2 := make([]int,0, 5) for i := 5; i < 10; i++ { i2 = append(i2, i) } copy(ints,i1) // 從i1全部復制到ints中 copy(ints[len(i1):],i2) // 將i2復制到ints的len(i1)位置開始一直到結束的數組中 fmt.Printf("%v\n",i1) fmt.Printf("%v\n",i2) fmt.Printf("%v\n",ints) } // output [0 1 2 3 4] [5 6 7 8 9] [0 1 2 3 4 5 6 7 8 9]
長度不夠的copy,依dist為準
package main import "fmt" func main() { ints := make([]int, 3) i1 := make([]int,0, 10) for i := 0; i < 10; i++ { i1 = append(i1, i) } copy(ints,i1) fmt.Printf("%v",ints) } //outPut: [0 1 2]
問題解答
nil 切片可以添加元素嗎?
可以
nil切片就是切片聲明,追加的時候切片長度為0,會引發(fā)擴容操作,擴容的時候會給分配一個新的數組。
nil切片和空切片有區(qū)別嗎?
nil切片有兩種方式
聲明new創(chuàng)建
空切片有兩種:
字面量創(chuàng)建但沒有任何的元素make創(chuàng)建長度指定為0
使用方式除了下面兩點沒有別的區(qū)別:
嵌套結構體,不顯性創(chuàng)建為niljson序列化會為null
slice擴容規(guī)則
說到前面:它在確定cap之后有內存對齊操作
小于256,是原cap的2倍大于256,是原來的1.25倍+3/4*256
到這里就結束了。
到此這篇關于GO中的slice詳解的文章就介紹到這了,更多相關go slice詳解內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
- 詳解Golang如何比較兩個slice是否相等
- GO語言基本類型String和Slice,Map操作詳解
- Go基礎系列:Go切片(分片)slice詳解
- golang?使用sort.slice包實現(xiàn)對象list排序
- 淺談Golang?Slice切片如何擴容的實現(xiàn)
- 淺談Golang 切片(slice)擴容機制的原理
- golang slice元素去重操作
- golang語言如何將interface轉為int, string,slice,struct等類型
- Golang中的Slice與數組及區(qū)別詳解
- Go 中 slice 的 In 功能實現(xiàn)探索
- Golang slice切片操作之切片的追加、刪除、插入等
相關文章
利用systemd部署golang項目的實現(xiàn)方法
這篇文章主要介紹了利用systemd部署golang項目的實現(xiàn)方法,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2019-11-11解決golang編譯提示dial tcp 172.217.160.113:443: con
這篇文章主要介紹了解決golang編譯提示dial tcp 172.217.160.113:443: connectex: A connection attempt failed,此問題完美解決,需要的朋友可以參考下2023-02-02Golang異常處理之defer,panic,recover的使用詳解
這篇文章主要為大家介紹了Go語言異常處理機制中defer、panic和recover三者的使用方法,文中示例代碼講解詳細,需要的朋友可以參考下2022-05-05