切片(slice)是 Golang 中一種比較特殊的數(shù)據(jù)結(jié)構(gòu)斗这,這種數(shù)據(jù)結(jié)構(gòu)更便于使用和管理數(shù)據(jù)集合轮纫。切片是圍繞動(dòng)態(tài)數(shù)組的概念構(gòu)建的洲拇,可以按需自動(dòng)增長(zhǎng)和縮小感论。切片的動(dòng)態(tài)增長(zhǎng)是通過(guò)內(nèi)置函數(shù) append() 來(lái)實(shí)現(xiàn)的绅项,這個(gè)函數(shù)可以快速且高效地增長(zhǎng)切片,也可以通過(guò)對(duì)切片再次切割比肄,縮小一個(gè)切片的大小快耿。因?yàn)榍衅牡讓右彩窃谶B續(xù)的內(nèi)存塊中分配的,所以切片還能獲得索引芳绩、迭代以及為垃圾回收優(yōu)化的好處掀亥。
本文將介紹 Golang 切片的基本概念和用法,演示環(huán)境為 ubuntu 18.04 & go1.10.1妥色。
切片的內(nèi)部實(shí)現(xiàn)
切片是一個(gè)很小的對(duì)象搪花,它對(duì)底層的數(shù)組(內(nèi)部是通過(guò)數(shù)組保存數(shù)據(jù)的)進(jìn)行了抽象,并提供相關(guān)的操作方法嘹害。切片是一個(gè)有三個(gè)字段的數(shù)據(jù)結(jié)構(gòu)撮竿,這些數(shù)據(jù)結(jié)構(gòu)包含 Golang 需要操作底層數(shù)組的元數(shù)據(jù):
這 3 個(gè)字段分別是指向底層數(shù)組的指針、切片訪(fǎng)問(wèn)的元素的個(gè)數(shù)(即長(zhǎng)度)和切片允許增長(zhǎng)到的元素個(gè)數(shù)(即容量)笔呀。
切片的創(chuàng)建和初始化
在 Golang 中可以通過(guò)多種方式創(chuàng)建和初始化切片幢踏。是否提前知道切片所需的容量通常會(huì)決定如何創(chuàng)建切片。
通過(guò) make() 函數(shù)創(chuàng)建切片
使用 Golang 內(nèi)置的 make() 函數(shù)創(chuàng)建切片许师,此時(shí)需要傳入一個(gè)參數(shù)來(lái)指定切片的長(zhǎng)度:
// 創(chuàng)建一個(gè)整型切片// 其長(zhǎng)度和容量都是 5 個(gè)元素slice?:=make([]int,5)
此時(shí)只指定了切片的長(zhǎng)度房蝉,那么切片的容量和長(zhǎng)度相等。也可以分別指定長(zhǎng)度和容量:
// 創(chuàng)建一個(gè)整型切片// 其長(zhǎng)度為 3 個(gè)元素枯跑,容量為 5 個(gè)元素slice?:=make([]int,3,5)
分別指定長(zhǎng)度和容量時(shí)惨驶,創(chuàng)建的切片,底層數(shù)組的長(zhǎng)度是指定的容量敛助,但是初始化后并不能訪(fǎng)問(wèn)所有的數(shù)組元素粗卜。
注意,Golang 不允許創(chuàng)建容量小于長(zhǎng)度的切片纳击,當(dāng)創(chuàng)建的切片容量小于長(zhǎng)度時(shí)會(huì)在編譯時(shí)刻報(bào)錯(cuò):
// 創(chuàng)建一個(gè)整型切片// 使其長(zhǎng)度大于容量myNum?:=make([]int,5,3)
編譯上面的代碼续扔,會(huì)收到下面的編譯錯(cuò)誤:
len larger than cap in make([]int)
通過(guò)字面量創(chuàng)建切片
另一種常用的創(chuàng)建切片的方法是使用切片字面量,這種方法和創(chuàng)建數(shù)組類(lèi)似焕数,只是不需要指定[]運(yùn)算符里的值纱昧。初始的長(zhǎng)度和容量會(huì)基于初始化時(shí)提供的元素的個(gè)數(shù)確定:
// 創(chuàng)建字符串切片// 其長(zhǎng)度和容量都是 3 個(gè)元素myStr?:=?[]string{"Jack","Mark","Nick"}// 創(chuàng)建一個(gè)整型切片// 其長(zhǎng)度和容量都是 4 個(gè)元素myNum?:=?[]int{10,20,30,40}
當(dāng)使用切片字面量創(chuàng)建切片時(shí),還可以設(shè)置初始長(zhǎng)度和容量堡赔。要做的就是在初始化時(shí)給出所需的長(zhǎng)度和容量作為索引旷痕。下面的語(yǔ)法展示了如何使用索引方式創(chuàng)建長(zhǎng)度和容量都是100個(gè)元素的切片:
// 創(chuàng)建字符串切片// 使用空字符串初始化第 100 個(gè)元素myStr?:=?[]string{99:""}
區(qū)分?jǐn)?shù)組的聲明和切片的聲明方式
當(dāng)使用字面量來(lái)聲明切片時(shí)乾胶,其語(yǔ)法與使用字面量聲明數(shù)組非常相似切心。二者的區(qū)別是:如果在 [] 運(yùn)算符里指定了一個(gè)值,那么創(chuàng)建的就是數(shù)組而不是切片离例。只有在 [] 中不指定值的時(shí)候,創(chuàng)建的才是切片悉稠」看下面的例子:
// 創(chuàng)建有 3 個(gè)元素的整型數(shù)組myArray?:=?[3]int{10,20,30}// 創(chuàng)建長(zhǎng)度和容量都是 3 的整型切片mySlice?:=?[]int{10,20,30}
nil 和空切片
有時(shí),程序可能需要聲明一個(gè)值為 nil 的切片(也稱(chēng)nil切片)的猛。只要在聲明時(shí)不做任何初始化耀盗,就會(huì)創(chuàng)建一個(gè) nil 切片
// 創(chuàng)建 nil 整型切片varmyNum []int
在 Golang 中,nil 切片是很常見(jiàn)的創(chuàng)建切片的方法卦尊。nil 切片可以用于很多標(biāo)準(zhǔn)庫(kù)和內(nèi)置函數(shù)叛拷。在需要描述一個(gè)不存在的切片時(shí),nil 切片會(huì)很好用猫牡。比如胡诗,函數(shù)要求返回一個(gè)切片但是發(fā)生異常的時(shí)候。下圖描述了 nil 切片的狀態(tài):
空切片和 nil 切片稍有不同淌友,下面的代碼分別通過(guò) make() 函數(shù)和字面量的方式創(chuàng)建空切片:
// 使用 make 創(chuàng)建空的整型切片myNum?:=make([]int,0)// 使用切片字面量創(chuàng)建空的整型切片myNum?:=?[]int{}
空切片的底層數(shù)組中包含 0 個(gè)元素,也沒(méi)有分配任何存儲(chǔ)空間骇陈。想表示空集合時(shí)空切片很有用震庭,比如,數(shù)據(jù)庫(kù)查詢(xún)返回 0 個(gè)查詢(xún)結(jié)果時(shí)你雌。下圖描述了空切片的狀態(tài):
不管是使用 nil 切片還是空切片器联,對(duì)其調(diào)用內(nèi)置函數(shù) append()、len() 和 cap() 的效果都是一樣的婿崭。
為切片中的元素賦值
對(duì)切片里某個(gè)索引指向的元素賦值和對(duì)數(shù)組里某個(gè)索引指向的元素賦值的方法完全一樣拨拓。使用 [] 操作符就可以改變某個(gè)元素的值,下面是使用切片字面量來(lái)聲明切片:
// 創(chuàng)建一個(gè)整型切片// 其容量和長(zhǎng)度都是 5 個(gè)元素myNum?:=?[]int{10,20,30,40,50}// 改變索引為 1 的元素的值myNum [1] =25
通過(guò)切片創(chuàng)建新的切片
切片之所以被稱(chēng)為切片氓栈,是因?yàn)閯?chuàng)建一個(gè)新的切片渣磷,也就是把底層數(shù)組切出一部分。通過(guò)切片創(chuàng)建新切片的語(yǔ)法如下:
slice[i:j]slice[i:j:k]
其中 i 表示從 slice 的第幾個(gè)元素開(kāi)始切授瘦,j 控制切片的長(zhǎng)度(j-i)醋界,k 控制切片的容量(k-i),如果沒(méi)有給定 k提完,則表示切到底層數(shù)組的最尾部形纺。下面是幾種常見(jiàn)的簡(jiǎn)寫(xiě)形式:
slice[i:]// 從 i 切到最尾部slice[:j]// 從最開(kāi)頭切到 j(不包含 j)slice[:]// 從頭切到尾,等價(jià)于復(fù)制整個(gè) slice
讓我們通過(guò)下面的例子來(lái)理解通過(guò)切片創(chuàng)建新的切片的本質(zhì):
// 創(chuàng)建一個(gè)整型切片// 其長(zhǎng)度和容量都是 5 個(gè)元素myNum?:=?[]int{10,20,30,40,50}// 創(chuàng)建一個(gè)新切片// 其長(zhǎng)度為 2 個(gè)元素徒欣,容量為 4 個(gè)元素newNum?:=?slice[1:3]
執(zhí)行上面的代碼后逐样,我們有了兩個(gè)切片,它們共享同一段底層數(shù)組,但通過(guò)不同的切片會(huì)看到底層數(shù)組的不同部分:
注意:截取新切片時(shí)的原則是 "左含右不含"脂新。所以 newNum 是從 myNum 的 index=1 處開(kāi)始截取秽澳,截取到 index=3 的前一個(gè)元素,也就是不包含 index=3 這個(gè)元素戏羽。所以担神,新的 newNum 是由 myNum 中的第2個(gè)元素、第3個(gè)元素組成的新的切片構(gòu)始花,長(zhǎng)度為 2妄讯,容量為 4。切片 myNum 能夠看到底層數(shù)組全部 5 個(gè)元素的容量酷宵,而 newNum 能看到的底層數(shù)組的容量只有 4 個(gè)元素亥贸。newNum 無(wú)法訪(fǎng)問(wèn)到底層數(shù)組的第一個(gè)元素。所以浇垦,對(duì) newNum 來(lái)說(shuō)炕置,那個(gè)元素就是不存在的。
共享底層數(shù)組的切片
需要注意的是:現(xiàn)在兩個(gè)切片 myNum 和 newNum 共享同一個(gè)底層數(shù)組男韧。如果一個(gè)切片修改了該底層數(shù)組的共享
部分朴摊,另一個(gè)切片也能感知到(請(qǐng)參考前圖):
// 修改 newNum 索引為 1 的元素// 同時(shí)也修改了原切片 myNum 的索引為 2 的元素newNum[1] =35
把 35 賦值給 newNum 索引為 1 的元素的同時(shí)也是在修改 myNum 索引為 2 的元素:
切片只能訪(fǎng)問(wèn)到其長(zhǎng)度內(nèi)的元素
切片只能訪(fǎng)問(wèn)到其長(zhǎng)度內(nèi)的元素,試圖訪(fǎng)問(wèn)超出其長(zhǎng)度的元素將會(huì)導(dǎo)致語(yǔ)言運(yùn)行時(shí)異常此虑。在使用這部分元素前甚纲,必須將其合并到切片的長(zhǎng)度里。下面的代碼試圖為 newNum 中的元素賦值:
// 修改 newNum 索引為 3 的元素// 這個(gè)元素對(duì)于 newNum 來(lái)說(shuō)并不存在newNum[3] =45
上面的代碼可以通過(guò)編譯朦前,但是會(huì)產(chǎn)生運(yùn)行時(shí)錯(cuò)誤:
panic:?runtime?error:?index?out?of?range
切片擴(kuò)容
相對(duì)于數(shù)組而言介杆,使用切片的一個(gè)好處是:可以按需增加切片的容量。Golang 內(nèi)置的 append() 函數(shù)會(huì)處理增加長(zhǎng)度時(shí)的所有操作細(xì)節(jié)韭寸。要使用 append() 函數(shù)春哨,需要一個(gè)被操作的切片和一個(gè)要追加的值,當(dāng) append() 函數(shù)返回時(shí)恩伺,會(huì)返回一個(gè)包含修改結(jié)果的新切片赴背。函數(shù) append() 總是會(huì)增加新切片的長(zhǎng)度,而容量有可能會(huì)改變莫其,也可能不會(huì)改變癞尚,這取決于被操作的切片的可用容量。
myNum?:=?[]int{10,20,30,40,50}// 創(chuàng)建新的切片乱陡,其長(zhǎng)度為 2 個(gè)元素浇揩,容量為 4 個(gè)元素newNum?:=?myNum[1:3]// 使用原有的容量來(lái)分配一個(gè)新元素// 將新元素賦值為 60newNum =append(newNum,60)
執(zhí)行上面的代碼后的底層數(shù)據(jù)結(jié)構(gòu)如下圖所示:
此時(shí)因?yàn)?newNum 在底層數(shù)組里還有額外的容量可用,append() 函數(shù)將可用的元素合并入切片的長(zhǎng)度憨颠,并對(duì)其進(jìn)行賦值胳徽。由于和原始的切片共享同一個(gè)底層數(shù)組积锅,myNum 中索引為 3 的元素的值也被改動(dòng)了。
如果切片的底層數(shù)組沒(méi)有足夠的可用容量养盗,append() 函數(shù)會(huì)創(chuàng)建一個(gè)新的底層數(shù)組缚陷,將被引用的現(xiàn)有的值復(fù)制到新數(shù)組里,再追加新的值往核,此時(shí) append 操作同時(shí)增加切片的長(zhǎng)度和容量:
// 創(chuàng)建一個(gè)長(zhǎng)度和容量都是 4 的整型切片myNum?:=?[]int{10,20,30,40}// 向切片追加一個(gè)新元素// 將新元素賦值為 50newNum?:=append(myNum,50)
當(dāng)這個(gè) append 操作完成后箫爷,newSlice 擁有一個(gè)全新的底層數(shù)組,這個(gè)數(shù)組的容量是原來(lái)的兩倍:
函數(shù) append() 會(huì)智能地處理底層數(shù)組的容量增長(zhǎng)聂儒。在切片的容量小于 1000 個(gè)元素時(shí)虎锚,總是會(huì)成倍地增加容量。一旦元素個(gè)數(shù)超過(guò) 1000衩婚,容量的增長(zhǎng)因子會(huì)設(shè)為 1.25窜护,也就是會(huì)每次增加 25%的容量(隨著語(yǔ)言的演化,這種增長(zhǎng)算法可能會(huì)有所改變)非春。
限制切片的容量
在創(chuàng)建切片時(shí)柱徙,使用第三個(gè)索引選項(xiàng)引可以用來(lái)控制新切片的容量。其目的并不是要增加容量奇昙,而是要限制容量护侮。允許限制新切片的容量為底層數(shù)組提供了一定的保護(hù),可以更好地控制追加操作敬矩。
// 創(chuàng)建長(zhǎng)度和容量都是 5 的字符串切片fruit?:=?[]string{"Apple","Orange","Plum","Banana","Grape"}
下面嘗試使用第三個(gè)索引項(xiàng)來(lái)完成切片操作:
// 將第三個(gè)元素切片概行,并限制容量// 其長(zhǎng)度為 1 個(gè)元素,容量為 2 個(gè)元素myFruit?:=?fruit[2:3:4]
這個(gè)切片操作執(zhí)行后弧岳,新切片里從底層數(shù)組引用了 1 個(gè)元素,容量是 2 個(gè)元素业踏。具體來(lái)說(shuō)禽炬,新切片引用了 Plum 元素,并將容量擴(kuò)展到 Banana 元素:
如果設(shè)置的容量比可用的容量還大勤家,就會(huì)得到一個(gè)運(yùn)行時(shí)錯(cuò)誤:
myFruit?:=?fruit[2:3:6]
panic:?runtime?error:?slice?bounds?out?of?range
內(nèi)置函數(shù) append() 在操作切片時(shí)會(huì)首先使用可用容量腹尖。一旦沒(méi)有可用容量,就會(huì)分配一個(gè)新的底層數(shù)組伐脖。這導(dǎo)致很容易忘記切片間正在共享同一個(gè)底層數(shù)組热幔。一旦發(fā)生這種情況,對(duì)切片進(jìn)行修改讼庇,很可能會(huì)導(dǎo)致隨機(jī)且奇怪的問(wèn)題绎巨,這種問(wèn)題一般都很難調(diào)查。如果在創(chuàng)建切片時(shí)設(shè)置切片的容量和長(zhǎng)度一樣蠕啄,就可以強(qiáng)制讓新切片的第一個(gè) append 操作創(chuàng)建新的底層數(shù)組场勤,與原有的底層數(shù)組分離戈锻。這樣就可以安全地進(jìn)行后續(xù)的修改操作了:
myFruit?:=?fruit[2:3:3]// 向 myFruit 追加新字符串myFruit = append(myFruit,"Kiwi")
這里,我們限制了 myFruit 的容量為 1和媳。當(dāng)我們第一次對(duì) myFruit 調(diào)用 append() 函數(shù)的時(shí)候格遭,會(huì)創(chuàng)建一個(gè)新的底層數(shù)組,這個(gè)數(shù)組包括 2 個(gè)元素留瞳,并將水果 Plum 復(fù)制進(jìn)來(lái)拒迅,再追加新水果 Kiwi,并返回一個(gè)引用了這個(gè)底層數(shù)組的新切片她倘。因?yàn)樾碌那衅?myFruit 擁有了自己的底層數(shù)組璧微,所以杜絕了可能發(fā)生的問(wèn)題。我們可以繼續(xù)向新切片里追加水果帝牡,而不用擔(dān)心會(huì)不小心修改了其他切片里的水果往毡。可以通過(guò)下圖來(lái)理解此時(shí)內(nèi)存中的數(shù)據(jù)結(jié)構(gòu):
將一個(gè)切片追加到另一個(gè)切片
內(nèi)置函數(shù) append() 也是一個(gè)可變參數(shù)的函數(shù)靶溜。這意味著可以在一次調(diào)用中傳遞多個(gè)值开瞭。如果使用 … 運(yùn)算符,可以將一個(gè)切片的所有元素追加到另一個(gè)切片里:
// 創(chuàng)建兩個(gè)切片罩息,并分別用兩個(gè)整數(shù)進(jìn)行初始化num1?:=?[]int{1,2}num2?:=?[]int{3,4}// 將兩個(gè)切片追加在一起嗤详,并顯示結(jié)果fmt.Printf("%v\n",append(num1, num2...))
輸出的結(jié)果為:
[1 2 3 4]
在返回的新的切片中,切片 num2 里的所有值都追加到了切片 num1 中的元素后面瓷炮。
遍歷切片
切片是一個(gè)集合葱色,可以迭代其中的元素。Golang 有個(gè)特殊的關(guān)鍵字 range娘香,它可以配合關(guān)鍵字 for 來(lái)迭代切片里的元素
myNum?:=?[]int{10,20,30,40,50}// 迭代每一個(gè)元素苍狰,并顯示其值forindex,?value?:=?range?myNum?{fmt.Printf("index:?%d?value:?%d\n",index, value)}
輸出的結(jié)果為:
index:0value:10index:1value:20index:2value:30index:3value:40index:4value:50
當(dāng)?shù)衅瑫r(shí),關(guān)鍵字 range 會(huì)返回兩個(gè)值烘绽。第一個(gè)值是當(dāng)前迭代到的索引位置淋昭,第二個(gè)值是該位置對(duì)應(yīng)元素值的一份副本。需要強(qiáng)調(diào)的是安接,range 創(chuàng)建了每個(gè)元素的副本翔忽,而不是直接返回對(duì)該元素的引用。要想獲取每個(gè)元素的地址盏檐,可以使用切片變量和索引值:
myNum?:=?[]int{10,20,30,40,50}// 修改切片元素的值// 使用空白標(biāo)識(shí)符(下劃線(xiàn))來(lái)忽略原始值forindex,?_?:=rangemyNum {myNum[index] +=1}forindex,?value?:=rangemyNum {fmt.Printf("index:?%d?value:?%d\n", index, value)}
輸出的結(jié)果為:
index:0value:11index:1value:21index:2value:31index:3value:41index:4value:51
關(guān)鍵字 range 總是會(huì)從切片頭部開(kāi)始遍歷歇式。如果想對(duì)遍歷做更多的控制,可以使用傳統(tǒng)的 for 循環(huán)配合 len() 函數(shù)實(shí)現(xiàn):
myNum?:=?[]int{10,20,30,40,50}// 從第三個(gè)元素開(kāi)始迭代每個(gè)元素forindex?:=2; index
切片間的拷貝操作
Golang 內(nèi)置的 copy() 函數(shù)可以將一個(gè)切片中的元素拷貝到另一個(gè)切片中胡野,其函數(shù)聲明為:
funccopy(dst, src []Type)int
它表示把切片 src 中的元素拷貝到切片 dst 中材失,返回值為拷貝成功的元素個(gè)數(shù)。如果 src 比 dst 長(zhǎng)给涕,就截?cái)嗖蜚荆蝗绻?src 比 dst 短额获,則只拷貝 src 那部分:
num1?:=?[]int{10,20,30}num2?:=make([]int,5)count?:=copy(num2, num1)fmt.Println(count)fmt.Println(num2)
運(yùn)行這段單面,輸出的結(jié)果為:
3[10203000]
3 表示拷貝成功的元素個(gè)數(shù)恭应。
把切片傳遞給函數(shù)
函數(shù)間傳遞切片就是要在函數(shù)間以值的方式傳遞切片抄邀。由于切片的尺寸很小,在函數(shù)間復(fù)制和傳遞切片成本也很低昼榛。
讓我們創(chuàng)建一個(gè)包含 100 萬(wàn)個(gè)整數(shù)的切片境肾,并將這個(gè)切片以值的方式傳遞給函數(shù) foo():
myNum?:=make([]int,1e6)// 將 myNum 傳遞到函數(shù) foo()slice = foo(myNum)// 函數(shù) foo() 接收一個(gè)整型切片,并返回這個(gè)切片funcfoo(slice []int)[]int{...returnslice}
在 64 位架構(gòu)的機(jī)器上胆屿,一個(gè)切片需要 24 字節(jié)的內(nèi)存:指針字段需要 8 字節(jié)奥喻,長(zhǎng)度和容量字段分別需要 8 字節(jié)。由于與切片關(guān)聯(lián)的數(shù)據(jù)包含在底層數(shù)組里非迹,不屬于切片本身环鲤,所以將切片復(fù)制到任意函數(shù)的時(shí)候,對(duì)底層數(shù)組大小都不會(huì)有影響憎兽。復(fù)制時(shí)只會(huì)復(fù)制切片本身冷离,不會(huì)涉及底層數(shù)組:
在函數(shù)間傳遞 24 字節(jié)的數(shù)據(jù)會(huì)非常快速纯命、簡(jiǎn)單西剥。這也是切片效率高的地方。不需要傳遞指針和處理復(fù)雜的語(yǔ)法亿汞,只需要復(fù)制切片瞭空,按想要的方式修改數(shù)據(jù),然后傳遞回一份新的切片副本疗我。
總結(jié)
切片是 Golang 中比較有特色的一種數(shù)據(jù)類(lèi)型咆畏,既為我們操作集合類(lèi)型的數(shù)據(jù)提供了便利的方式,又能夠高效的在函數(shù)間進(jìn)行傳遞吴裤,因此在代碼中切片類(lèi)型被使用的相當(dāng)廣泛鳖眼。