Go基礎(chǔ)系列:Go切片(分片)slice詳解
slice表示切片(分片),例如對一個數(shù)組進(jìn)行切片,取出數(shù)組中的一部分值。在現(xiàn)代編程語言中,slice(切片)幾乎成為一種必備特性,它可以從一個數(shù)組(列表)中取出任意長度的子數(shù)組(列表),為操作數(shù)據(jù)結(jié)構(gòu)帶來非常大的便利性,如python、perl等都支持對數(shù)組的slice操作,甚至perl還支持對hash數(shù)據(jù)結(jié)構(gòu)的slice。
但Go中的slice和這些語言的slice不太一樣,前面所說的語言中,slice是一種切片的操作,切片后返回一個新的數(shù)據(jù)對象。而Go中的slice不僅僅是一種切片動作,還是一種數(shù)據(jù)結(jié)構(gòu)(就像數(shù)組一樣)。
slice的存儲結(jié)構(gòu)
Go中的slice依賴于數(shù)組,它的底層就是數(shù)組,所以數(shù)組具有的優(yōu)點(diǎn),slice都有。且slice支持可以通過append向slice中追加元素,長度不夠時會動態(tài)擴(kuò)展,通過再次slice切片,可以得到得到更小的slice結(jié)構(gòu),可以迭代、遍歷等。
實際上slice是這樣的結(jié)構(gòu):先創(chuàng)建一個有特定長度和數(shù)據(jù)類型的底層數(shù)組,然后從這個底層數(shù)組中選取一部分元素,返回這些元素組成的集合(或容器),并將slice指向集合中的第一個元素。換句話說,slice自身維護(hù)了一個指針屬性,指向它底層數(shù)組中的某些元素的集合。
例如,初始化一個slice數(shù)據(jù)結(jié)構(gòu):
my_slice := make([]int, 3,5) // 輸出slice fmt.Println(my_slice) // 輸出:[0 0 0]
這表示先聲明一個長度為5、數(shù)據(jù)類型為int的底層數(shù)組,然后從這個底層數(shù)組中從前向后取3個元素(即index從0到2)作為slice的結(jié)構(gòu)。
如下圖:
每一個slice結(jié)構(gòu)都由3部分組成:容量(capacity)、長度(length)和指向底層數(shù)組某元素的指針,它們各占8字節(jié)(1個機(jī)器字長,64位機(jī)器上一個機(jī)器字長為64bit,共8字節(jié)大小,32位架構(gòu)則是32bit,占用4字節(jié)),所以任何一個slice都是24字節(jié)(3個機(jī)器字長)。
- Pointer:表示該slice結(jié)構(gòu)從底層數(shù)組的哪一個元素開始,該指針指向該元素
- Capacity:即底層數(shù)組的長度,表示這個slice目前最多能擴(kuò)展到這么長
- Length:表示slice當(dāng)前的長度,如果追加元素,長度不夠時會擴(kuò)展,最大擴(kuò)展到Capacity的長度(不完全準(zhǔn)確,后面數(shù)組自動擴(kuò)展時解釋),所以Length必須不能比Capacity更大,否則會報錯
對上面創(chuàng)建的slice來說,它的長度為3,容量為5,指針指向底層數(shù)組的index=0。
可以通過len()函數(shù)獲取slice的長度,通過cap()函數(shù)獲取slice的Capacity。
my_slice := make([]int,3,5) fmt.Println(len(my_slice)) // 3 fmt.Println(cap(my_slice)) // 5
還可以直接通過print()或println()函數(shù)去輸出slice,它將得到這個slice結(jié)構(gòu)的屬性值,也就是length、capacity和pointer:
my_slice := make([]int,3,5) println(my_slice) // [3/5]0xc42003df10
[3/5]
表示length和capacity,0xc42003df10
表示指向的底層數(shù)組元素的指針。
務(wù)必記住slice的本質(zhì)是[x/y]0xADDR
,記住它將在很多地方有助于理解slice的特性。另外,個人建議,雖然slice的本質(zhì)不是指針,但仍然可以將它看作是一種包含了另外兩種屬性的不純粹的指針,也就是說,直接認(rèn)為它是指針。其實不僅slice如此,map也如此。
創(chuàng)建、初始化、訪問slice
有幾種創(chuàng)建slice數(shù)據(jù)結(jié)構(gòu)的方式。
一種是使用make():
// 創(chuàng)建一個length和capacity都等于5的slice slice := make([]int,5) // length=3,capacity=5的slice slice := make([]int,3,5)
make()比new()函數(shù)多一些操作,new()函數(shù)只會進(jìn)行內(nèi)存分配并做默認(rèn)的賦0初始化,而make()可以先為底層數(shù)組分配好內(nèi)存,然后從這個底層數(shù)組中再額外生成一個slice并初始化。另外,make只能構(gòu)建slice、map和channel這3種結(jié)構(gòu)的數(shù)據(jù)對象,因為它們都指向底層數(shù)據(jù)結(jié)構(gòu),都需要先為底層數(shù)據(jù)結(jié)構(gòu)分配好內(nèi)存并初始化。
還可以直接賦值初始化的方式創(chuàng)建slice:
// 創(chuàng)建長度和容量都為4的slice,并初始化賦值 color_slice := []string{"red","blue","black","green"} // 創(chuàng)建長度和容量為100的slice,并為第100個元素賦值為3 slice := []int{99:3}
注意區(qū)分array和slice:
// 創(chuàng)建長度為3的int數(shù)組 array := [3]int{10, 20, 30} // 創(chuàng)建長度和容量都為3的slice slice := []int{10, 20, 30}
由于slice底層是數(shù)組,所以可以使用索引的方式訪問slice,或修改slice中元素的值:
// 創(chuàng)建長度為5、容量為5的slice my_slice := []int{11,22,33,44,55} // 訪問slice的第2個元素 print(my_slice[1]) // 修改slice的第3個元素的值 my_slice[2] = 333
由于slice的底層是數(shù)組,所以訪問my_slice[1]
實際上是在訪問它的底層數(shù)組的對應(yīng)元素。slice能被訪問的元素只有l(wèi)ength范圍內(nèi)的元素,那些在length之外,但在capacity之內(nèi)的元素暫時還不屬于slice,只有在slice被擴(kuò)展時(見下文append),capacity中的元素才被納入length,才能被訪問。
nil slice和空slice
當(dāng)聲明一個slice,但不做初始化的時候,這個slice就是一個nil slice。
// 聲明一個nil slice var nil_slice []int
nil slice表示它的指針為nil,也就是這個slice不會指向哪個底層數(shù)組。也因此,nil slice的長度和容量都為0。
|--------|---------|----------| | nil | 0 | 0 | | ptr | Length | Capacity | |--------|---------|----------|
還可以創(chuàng)建空slice(Empty Slice),空slice表示長度為0,容量為0,但卻有指向的slice,只不過指向的底層數(shù)組暫時是長度為0的空數(shù)組。
// 使用make創(chuàng)建 empty_slice := make([]int,0) // 直接創(chuàng)建 empty_slice := []int{}
Empty Slice的結(jié)構(gòu)如下:
|--------|---------|----------| | ADDR | 0 | 0 | | ptr | Length | Capacity | |--------|---------|----------|
雖然nil slice和Empty slice的長度和容量都為0,輸出時的結(jié)果都是[]
,且都不存儲任何數(shù)據(jù),但它們是不同的。nil slice不會指向底層數(shù)組,而空slice會指向底層數(shù)組,只不過這個底層數(shù)組暫時是空數(shù)組。
可以使用println()來輸出驗證:
package main func main() { var nil_s []int empty_s:= []int{} println(nil_s) println(empty_s) }
輸出結(jié)果:
[0/0]0x0 [0/0]0xc042085f50
當(dāng)然,無論是nil slice還是empty slice,都可以對它們進(jìn)行操作,如append()函數(shù)、len()函數(shù)和cap()函數(shù)。
對slice進(jìn)行切片
可以從slice中繼續(xù)切片生成一個新的slice,這樣能實現(xiàn)slice的縮減。
對slice切片的語法為:
SLICE[A:B] SLICE[A:B:C]
其中A表示從SLICE的第幾個元素開始切,B控制切片的長度(B-A),C控制切片的容量(C-A),如果沒有給定C,則表示切到底層數(shù)組的最尾部。
還有幾種簡化形式:
SLICE[A:] // 從A切到最尾部 SLICE[:B] // 從最開頭切到B(不包含B) SLICE[:] // 從頭切到尾,等價于復(fù)制整個SLICE
例如:
my_slice := []int{11,22,33,44,55} // 生成新的slice,從第二個元素取,切取的長度為2 new_slice := my_slice[1:3]
注意,截取時"左閉右開"。所以上面new_slice
是從my_slice
的index=1開始截取,截取到index=3為止,但不包括index=3這個元素。所以,新的slice是由my_slice
中的第2個元素、第3個元素組成的新的數(shù)據(jù)結(jié)構(gòu),長度為2。
以下是slice切片生成新的slice后的結(jié)構(gòu):
不難發(fā)現(xiàn),一個底層數(shù)組,可以生成無數(shù)個slice,且對于new_slice而言,它并不知道底層數(shù)組index=0的那個元素。
還可以控制切片時新slice的容量:
my_slice := []int{11,22,33,44,55} // 從第二個元素取,切取的長度為2,容量也為2 new_slice := my_slice[1:3:3]
這時新slice的length等于capacity,底層數(shù)組的index=4、5將對new_slice永不可見,即使后面對new_slice進(jìn)行append()導(dǎo)致底層數(shù)組擴(kuò)容也仍然不可見。具體見下文。
由于多個slice共享同一個底層數(shù)組,所以當(dāng)修改了某個slice中的元素時,其它包含該元素的slice也會隨之改變,因為slice只是一個指向底層數(shù)組的指針(只不過這個指針不純粹,多了兩個額外的屬性length和capacity),實際上修改的是底層數(shù)組的值,而底層數(shù)組是被共享的。
當(dāng)同一個底層數(shù)組有很多slice的時候,一切將變得混亂不堪,因為我們不可能記住誰在共享它,通過修改某個slice的元素時,將也會影響那些可能我們不想影響的slice。所以,需要一種特性,保證各個slice的底層數(shù)組互不影響,相關(guān)內(nèi)容見下面的"擴(kuò)容"。
copy()函數(shù)
可以將一個slice拷貝到另一個slice中。
$ go doc builtin copy func copy(dst, src []Type) int
這表示將src slice拷貝到dst slice,src比dst長,就截斷,src比dst短,則只拷貝src那部分。
copy的返回值是拷貝成功的元素數(shù)量,所以也就是src slice或dst slice中最小的那個長度。
例如:
s1 := []int{11, 22, 33} s2 := make([]int, 5) s3 := make([]int,2) num := copy(s2, s1) copy(s3,s1) fmt.Println(num) // 3 fmt.Println(s2) // [11,22,33,0,0] fmt.Println(s3) // [11,22]
此外,copy還能將字符串拷貝到byte slice中,因為字符串實際上就是[]byte
。
func main() { s1 := []byte("Hello") num := copy(s1, "World") fmt.Println(num) fmt.Println(s1) // 輸出[87 111 114 108 100 32] fmt.Println(string(s1)) //輸出"World" }
append()函數(shù)
可以使用append()函數(shù)對slice進(jìn)行擴(kuò)展,因為它追加元素到slice中,所以一定會增加slice的長度。
但必須注意,append()的結(jié)果必須被使用。所謂被使用,可以將其輸出、可以賦值給某個slice。如果將append()放在空上下文將會報錯:append()已評估,但未使用。同時這也說明,append()返回一個新的slice,原始的slice會保留不變。
例如:
my_slice := []int{11,22,33,44,55} new_slice := my_slice[1:3] // append()追加一個元素2323,返回新的slice app_slice := append(new_slice,2323)
上面的append()在new_slice
的后面增加了一個元素2323,所以app_slice[2]=2323
。但因為這些slice共享同一個底層數(shù)組,所以2323也會反映到其它slice中。
現(xiàn)在的數(shù)據(jù)結(jié)構(gòu)圖如下:
當(dāng)然,如果append()的結(jié)果重新賦值給new_slice,則new_slice
會增加長度。
同樣,由于string的本質(zhì)是[]byte,所以string可以append到byte slice中:
s1 := []byte("Hello") s2 := append(s1, "World"...) fmt.Println(string(s2)) // 輸出:HelloWorld
擴(kuò)容
當(dāng)slice的length已經(jīng)等于capacity的時候,再使用append()給slice追加元素,會自動擴(kuò)展底層數(shù)組的長度。
底層數(shù)組擴(kuò)展時,會生成一個新的底層數(shù)組。所以舊底層數(shù)組仍然會被舊slice引用,新slice和舊slice不再共享同一個底層數(shù)組。
func main() { my_slice := []int{11,22,33,44,55} new_slice := append(my_slice,66) my_slice[3] = 444 // 修改舊的底層數(shù)組 fmt.Println(my_slice) // [11 22 33 444 55] fmt.Println(new_slice) // [11 22 33 44 55 66] fmt.Println(len(my_slice),":",cap(my_slice)) // 5:5 fmt.Println(len(new_slice),":",cap(new_slice)) // 6:10 }
從上面的結(jié)果上可以發(fā)現(xiàn),底層數(shù)組被擴(kuò)容為10,且是新的底層數(shù)組。
實際上,當(dāng)?shù)讓訑?shù)組需要擴(kuò)容時,會按照當(dāng)前底層數(shù)組長度的2倍進(jìn)行擴(kuò)容,并生成新數(shù)組。如果底層數(shù)組的長度超過1000時,將按照25%的比率擴(kuò)容,也就是1000個元素時,將擴(kuò)展為1250個,不過這個增長比率的算法可能會隨著go版本的遞進(jìn)而改變。
實際上,上面的說法應(yīng)該改一改:當(dāng)capacity需要擴(kuò)容時,會按照當(dāng)前capacity的2倍對數(shù)組進(jìn)行擴(kuò)容?;蛘哒f,是按照slice的本質(zhì)[x/y]0xADDR
的容量y來判斷如何擴(kuò)容的。之所以要特別強(qiáng)調(diào)這兩種不同,是因為很容易搞混。
例如,擴(kuò)容的對象是底層數(shù)組的真子集時:
my_slice := []int{11,22,33,44,55} // 限定長度和容量,且讓長度和容量相等 new_slice := my_slice[1:3:3] // [22 33] // 擴(kuò)容 app_slice := append(new_slice,4444)
上面的new_slice
的容量為2,并沒有對應(yīng)到底層數(shù)組的最結(jié)尾,所以new_slice
是my_slice
的一個真子集。這時對new_slice
擴(kuò)容,將生成一個新的底層數(shù)組,新的底層數(shù)組容量為4,而不是10。如下圖:
因為創(chuàng)建了新的底層數(shù)組,所以修改不同的slice,將不會互相影響。為了保證每次都是修改各自的底層數(shù)組,通常會切出僅一個長度、僅一個容量的新slice,這樣只要對它進(jìn)行任何一次擴(kuò)容,就會生成一個新的底層數(shù)組,從而讓每個slice的底層數(shù)組都獨(dú)立。
my_slice := []int{11,22,33,44,55} new_slice := my_slice[2:3:3] app_slice := append(new_slice,3333)
事實上,這還是會出現(xiàn)共享的幾率,因為沒有擴(kuò)容時,那個唯一的元素仍然是共享的,修改它肯定會影響至少1個slice,只有切出長度為0,容量為0的slice,才能完全保證獨(dú)立性,但這和新創(chuàng)建一個slice沒有區(qū)別。
合并slice
slice和數(shù)組其實一樣,都是一種值,可以將一個slice和另一個slice進(jìn)行合并,生成一個新的slice。
合并slice時,只需將append()的第二個參數(shù)后加上...
即可,即append(s1,s2...)
表示將s2合并在s1的后面,并返回新的slice。
s1 := []int{1,2} s2 := []int{3,4} s3 := append(s1,s2...) fmt.Println(s3) // [1 2 3 4]
注意append()最多允許兩個參數(shù),所以一次性只能合并兩個slice。但可以取巧,將append()作為另一個append()的參數(shù),從而實現(xiàn)多級合并。例如,下面的合并s1和s2,然后再和s3合并,得到s1+s2+s3
合并后的結(jié)果。
sn := append(append(s1,s2...),s3...)
slice遍歷迭代
slice是一個集合,所以可以進(jìn)行迭代。
range關(guān)鍵字可以對slice進(jìn)行迭代,每次返回一個index和對應(yīng)的元素值。可以將range的迭代結(jié)合for循環(huán)對slice進(jìn)行遍歷。
package main func main() { s1 := []int{11,22,33,44} for index,value := range s1 { println("index:",index," , ","value",value) } }
輸出結(jié)果:
index: 0 , value 11 index: 1 , value 22 index: 2 , value 33 index: 3 , value 44
傳遞slice給函數(shù)
前面說過,雖然slice實際上包含了3個屬性,它的數(shù)據(jù)結(jié)構(gòu)類似于[3/5]0xc42003df10
,但仍可以將slice看作一種指針。這個特性直接體現(xiàn)在函數(shù)參數(shù)傳值上。
Go中函數(shù)的參數(shù)是按值傳遞的,所以調(diào)用函數(shù)時會復(fù)制一個參數(shù)的副本傳遞給函數(shù)。如果傳遞給函數(shù)的是slice,它將復(fù)制該slice副本給函數(shù),這個副本實際上就是[3/5]0xc42003df10
,所以傳遞給函數(shù)的副本仍然指向源slice的底層數(shù)組。
換句話說,如果函數(shù)內(nèi)部對slice進(jìn)行了修改,有可能會直接影響函數(shù)外部的底層數(shù)組,從而影響其它slice。但并不總是如此,例如函數(shù)內(nèi)部對slice進(jìn)行擴(kuò)容,擴(kuò)容時生成了一個新的底層數(shù)組,函數(shù)后續(xù)的代碼只對新的底層數(shù)組操作,這樣就不會影響原始的底層數(shù)組。
例如:
package main import "fmt" func main() { s1 := []int{11, 22, 33, 44} foo(s1) fmt.Println(s1[1]) // 輸出:23 } func foo(s []int) { for index, _ := range s { s[index] += 1 } }
上面將輸出23,因為foo()直接操作原始的底層數(shù)組,對slice的每個元素都加1。
slice和內(nèi)存浪費(fèi)問題
由于slice的底層是數(shù)組,很可能數(shù)組很大,但slice所取的元素數(shù)量卻很小,這就導(dǎo)致數(shù)組占用的絕大多數(shù)空間是被浪費(fèi)的。
特別地,垃圾回收器(GC)不會回收正在被引用的對象,當(dāng)一個函數(shù)直接返回指向底層數(shù)組的slice時,這個底層數(shù)組將不會隨函數(shù)退出而被回收,而是因為slice的引用而永遠(yuǎn)保留,除非返回的slice也消失。
因此,當(dāng)函數(shù)的返回值是一個指向底層數(shù)組的數(shù)據(jù)結(jié)構(gòu)時(如slice),應(yīng)當(dāng)在函數(shù)內(nèi)部將slice拷貝一份保存到一個使用自己底層數(shù)組的新slice中,并返回這個新的slice。這樣函數(shù)一退出,原來那個體積較大的底層數(shù)組就會被回收,保留在內(nèi)存中的是小的slice。
更多關(guān)于Go語言切片(分片)slice的技術(shù)文章請查看下面的相關(guān)鏈接
- 詳解Golang如何比較兩個slice是否相等
- GO語言基本類型String和Slice,Map操作詳解
- golang?使用sort.slice包實現(xiàn)對象list排序
- 淺談Golang?Slice切片如何擴(kuò)容的實現(xiàn)
- 淺談Golang 切片(slice)擴(kuò)容機(jī)制的原理
- golang slice元素去重操作
- golang語言如何將interface轉(zhuǎn)為int, string,slice,struct等類型
- Golang中的Slice與數(shù)組及區(qū)別詳解
- Go 中 slice 的 In 功能實現(xiàn)探索
- Golang slice切片操作之切片的追加、刪除、插入等
- GO中的slice使用簡介(源碼分析slice)
相關(guān)文章
一文帶你掌握Go語言并發(fā)模式中的Context的上下文管理
在?Go?的日常開發(fā)中,Context?上下文對象無處不在,無論是處理網(wǎng)絡(luò)請求、數(shù)據(jù)庫操作還是調(diào)用?RPC?等場景,那你真的熟悉它的正確用法嗎,隨著本文一探究竟吧2023-05-05golang gin 監(jiān)聽rabbitmq隊列無限消費(fèi)的案例代碼
這篇文章主要介紹了golang gin 監(jiān)聽rabbitmq隊列無限消費(fèi),本文通過實例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2022-12-12golang gin 框架 異步同步 goroutine 并發(fā)操作
這篇文章主要介紹了golang gin 框架 異步同步 goroutine 并發(fā)操作,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-12-12實現(xiàn)像php一樣方便的go ORM數(shù)據(jù)庫操作示例詳解
這篇文章主要為大家介紹了實現(xiàn)像php一樣方便的go ORM數(shù)據(jù)庫操作示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-12-12