你不知道的 Go 之 slice

簡(jiǎn)介

切片(slice)是 Go 語(yǔ)言提供的一種數(shù)據(jù)結(jié)構(gòu)祟偷,使用非常簡(jiǎn)單、便捷毒坛。但是由于實(shí)現(xiàn)層面的原因,切片也經(jīng)常會(huì)產(chǎn)生讓人疑惑的結(jié)果林说。掌握切片的底層結(jié)構(gòu)和原理煎殷,可以避免很多常見(jiàn)的使用誤區(qū)。

底層結(jié)構(gòu)

切片結(jié)構(gòu)定義在源碼runtime包下的 slice.go 文件中:

// src/runtime/slice.go
type slice struct {
  array unsafe.Pointer
  len int
  cap int
}
  • array:一個(gè)指針腿箩,指向底層存儲(chǔ)數(shù)據(jù)的數(shù)組
  • len:切片的長(zhǎng)度豪直,在代碼中我們可以使用len()函數(shù)獲取這個(gè)值
  • cap:切片的容量,即在不擴(kuò)容的情況下珠移,最多能容納多少元素弓乙。在代碼中我們可以使用cap()函數(shù)獲取這個(gè)值

[圖片上傳失敗...(image-8752db-1622475715538)]

我們可以通過(guò)下面的代碼輸出切片的底層結(jié)構(gòu):

type slice struct {
  array unsafe.Pointer
  len   int
  cap   int
}

func printSlice() {
  s := make([]uint32, 1, 10)
  fmt.Printf("%#v\n", *(*slice)(unsafe.Pointer(&s)))
}

func main() {
  printSlice()
}

運(yùn)行輸出:

main.slice{array:(unsafe.Pointer)(0xc0000d6030), len:1, cap:10}

這里注意一個(gè)細(xì)節(jié),由于runtime.slice結(jié)構(gòu)是非導(dǎo)出的钧惧,我們不能直接使用暇韧。所以我在代碼中手動(dòng)定義了一個(gè)slice結(jié)構(gòu)體,字段與runtime.slice結(jié)構(gòu)相同浓瞪。

我們結(jié)合切片的底層結(jié)構(gòu)懈玻,先回顧一下切片的基礎(chǔ)知識(shí),然后再逐一看看切片的常見(jiàn)問(wèn)題追逮。

基礎(chǔ)知識(shí)

創(chuàng)建切片

創(chuàng)建切片有 4 種方式:

  1. var

var聲明切片類型的變量酪刀,這時(shí)切片值為nil粹舵。

var s []uint32

這種方式創(chuàng)建的切片,array字段為空指針骂倘,lencap字段都等于 0眼滤。

  1. 切片字面量

使用切片字面量將所有元素都列舉出來(lái),這時(shí)切片長(zhǎng)度和容量都等于指定元素的個(gè)數(shù)历涝。

s := []uint32{1, 2, 3}

創(chuàng)建之后s的底層結(jié)構(gòu)如下:

[圖片上傳失敗...(image-8f4838-1622475715538)]

lencap字段都等于 3诅需。

  1. make

使用make創(chuàng)建,可以指定長(zhǎng)度和容量荧库。格式為make([]type, len[, cap])堰塌,可以只指定長(zhǎng)度,也可以長(zhǎng)度容量同時(shí)指定:

s1 := make([]uint32)
s2 := make([]uint32, 1)
s3 := make([]uint32, 1, 10)
  1. 切片操作符

使用切片操作符可以從現(xiàn)有的切片或數(shù)組中切取一部分分衫,創(chuàng)建一個(gè)新的切片场刑。切片操作符格式為[low:high],例如:

var arr [10]uint32
s1 := arr[0:5]
s2 := arr[:5]
s3 := arr[5:]
s4 := arr[:]

區(qū)間是左開(kāi)右閉的蚪战,即[low, high)牵现,包括索引low,不包括high邀桑。切取生成的切片長(zhǎng)度為high-low瞎疼。

另外lowhigh都有默認(rèn)值。low默認(rèn)為 0壁畸,high默認(rèn)為原切片或數(shù)組的長(zhǎng)度贼急。它們都可以省略,省略時(shí)捏萍,相當(dāng)于取默認(rèn)值太抓。

使用這種方式創(chuàng)建的切片底層共享相同的數(shù)據(jù)空間,在進(jìn)行切片操作時(shí)可能會(huì)造成數(shù)據(jù)覆蓋照弥,要格外小心腻异。

添加元素

可以使用append()函數(shù)向切片中添加元素,可以一次添加 0 個(gè)或多個(gè)元素这揣。如果剩余空間(即cap-len)足夠存放元素則直接將元素添加到后面悔常,然后增加字段len的值即可。反之给赞,則需要擴(kuò)容机打,分配一個(gè)更大的數(shù)組空間,將舊數(shù)組中的元素復(fù)制過(guò)去片迅,再執(zhí)行添加操作残邀。

package main

import "fmt"

func main() {
  s := make([]uint32, 0, 4)

  s = append(s, 1, 2, 3)
  fmt.Println(len(s), cap(s)) // 3 4

  s = append(s, 4, 5, 6)
  fmt.Println(len(s), cap(s)) // 6 8
}

你不知道的 slice

  1. 空切片等于nil嗎?

下面代碼的輸出什么?

func main() {
  var s1 []uint32
  s2 := make([]uint32, 0)

  fmt.Println(s1 == nil)
  fmt.Println(s2 == nil)
  fmt.Println("nil slice:", len(s1), cap(s1))
  fmt.Println("cap slice:", len(s2), cap(s2))
}

分析:

首先s1s2的長(zhǎng)度和容量都為 0芥挣,這很好理解驱闷。比較切片與nil是否相等,實(shí)際上要檢查slice結(jié)構(gòu)中的array字段是否是空指針空免。顯然s1 == nil返回true空另,s2 == nil返回false。盡管s2長(zhǎng)度為 0蹋砚,但是make()為它分配了空間扼菠。所以,一般定義長(zhǎng)度為 0 的切片使用var的形式坝咐。

  1. 傳值還是傳引用循榆?

下面代碼的輸出什么?

func main() {
  s1 := []uint32{1, 2, 3}
  s2 := append(s1, 4)

  fmt.Println(s1)
  fmt.Println(s2)
}

分析:

為什么append()函數(shù)要有返回值墨坚?因?yàn)槲覀儗⑶衅瑐鬟f給append()時(shí)秧饮,其實(shí)傳入的是runtime.slice結(jié)構(gòu)。這個(gè)結(jié)構(gòu)是按值傳遞的框杜,所以函數(shù)內(nèi)部對(duì)array/len/cap這幾個(gè)字段的修改都不影響外面的切片結(jié)構(gòu)浦楣。上面代碼中,執(zhí)行append()之后s1lencap保持不變咪辱,故輸出為:

[1 2 3]
[1 2 3 4]

所以我們調(diào)用append()要寫成s = append(s, elem)這種形式,將返回值賦值給原切片椎组,從而覆寫array/len/cap這幾個(gè)字段的值油狂。

初學(xué)者還可能會(huì)犯忽略append()返回值的錯(cuò)誤:

append(s, elem)

這就更加大錯(cuò)特錯(cuò)了。添加的元素將會(huì)丟失寸癌,以為函數(shù)外切片的內(nèi)部字段都沒(méi)有變化专筷。

我們可以看到,雖說(shuō)切片是按引用傳遞的蒸苇,但是實(shí)際上傳遞的是結(jié)構(gòu)runtime.slice的值磷蛹。只是對(duì)現(xiàn)有元素的修改會(huì)反應(yīng)到函數(shù)外,因?yàn)榈讓訑?shù)組空間是共用的溪烤。

  1. 切片的擴(kuò)容策略

下面代碼的輸出是什么味咳?

func main() {
  var s1 []uint32
  s1 = append(s1, 1, 2, 3)
  s2 := append(s1, 4)
  fmt.Println(&s1[0] == &s2[0])
}

這涉及到切片的擴(kuò)容策略。擴(kuò)容時(shí)檬嘀,若:

  • 當(dāng)前容量小于 1024槽驶,則將容量擴(kuò)大為原來(lái)的 2 倍;
  • 當(dāng)前容量大于等于 1024鸳兽,則將容量逐次增加原來(lái)的 0.25 倍掂铐,直到滿足所需容量。

我翻看了 Go1.16 版本runtime/slice.go中擴(kuò)容相關(guān)的源碼,在執(zhí)行上面規(guī)則后還會(huì)根據(jù)切片元素的大小和計(jì)算機(jī)位數(shù)進(jìn)行相應(yīng)的調(diào)整全陨。整個(gè)過(guò)程比較復(fù)雜爆班,感興趣可以自行去研究。

我們只需要知道一開(kāi)始容量較小辱姨,擴(kuò)大為 2 倍柿菩,降低后續(xù)因添加元素導(dǎo)致擴(kuò)容的頻次。容量擴(kuò)張到一定程度時(shí)炮叶,再按照 2 倍來(lái)擴(kuò)容會(huì)造成比較大的浪費(fèi)碗旅。

上面例子中執(zhí)行s1 = append(s1, 1, 2, 3)后,容量會(huì)擴(kuò)大為 4镜悉。再執(zhí)行s2 := append(s1, 4)由于有足夠的空間祟辟,s2底層的數(shù)組不會(huì)改變。所以s1s2第一個(gè)元素的地址相同侣肄。

  1. 切片操作符可以切取字符串

切片操作符可以切取字符串旧困,但是與切取切片和數(shù)組不同。切取字符串返回的是字符串稼锅,而非切片吼具。因?yàn)樽址遣豢勺兊模绻祷厍衅鼐唷6衅妥址蚕淼讓訑?shù)據(jù)拗盒,就可以通過(guò)切片修改字符串了。

func main() {
  str := "hello, world"
  fmt.Println(str[:5])
}

輸出 hello锥债。

  1. 切片底層數(shù)據(jù)共享

下面代碼的輸出是什么陡蝇?

func main() {
  array := [10]uint32{1, 2, 3, 4, 5}
  s1 := array[:5]

  s2 := s1[5:10]
  fmt.Println(s2)

  s1 = append(s1, 6)
  fmt.Println(s1)
  fmt.Println(s2)
}

分析:

首先注意到s2 := s1[5:10]上界 10 已經(jīng)大于切片s1的長(zhǎng)度了。要記住哮肚,使用切片操作符切取切片時(shí)登夫,上界是切片的容量,而非長(zhǎng)度允趟。這時(shí)兩個(gè)切片的底層結(jié)構(gòu)有重疊恼策,如下圖:

[圖片上傳失敗...(image-687c90-1622475715538)]

這時(shí)輸出s2為:

[0, 0, 0, 0, 0]

然后向切片s1中添加元素 6,這時(shí)結(jié)構(gòu)如下圖潮剪,其中切片s1s2共享元素 6:

[圖片上傳失敗...(image-2467db-1622475715538)]

這時(shí)輸出的s1s2為:

[1, 2, 3, 4, 5, 6]
[6, 0, 0, 0, 0]

可以看到由于切片底層數(shù)據(jù)共享可能造成修改一個(gè)切片會(huì)導(dǎo)致其他切片也跟著修改涣楷。這有時(shí)會(huì)造成難以調(diào)試的 BUG。為了一定程度上緩解這個(gè)問(wèn)題鲁纠,Go 1.2 版本中提供了一個(gè)擴(kuò)展切片操作符:[low:high:max]总棵,用來(lái)限制新切片的容量。使用這種方式產(chǎn)生的切片容量為max-low改含。

func main() {
  array := [10]uint32{1, 2, 3, 4, 5}
  s1 := array[:5:5]

  s2 := array[5:10:10]
  fmt.Println(s2)

  s1 = append(s1, 6)
  fmt.Println(s1)
  fmt.Println(s2)
}

執(zhí)行s1 := array[:5:5]我們限定了s1的容量為 5情龄,這時(shí)結(jié)構(gòu)如下圖所示:

[圖片上傳失敗...(image-c59a71-1622475715538)]

執(zhí)行s1 = append(s1, 6)時(shí),發(fā)現(xiàn)沒(méi)有空閑容量了(因?yàn)?code>len == cap == 5),重新創(chuàng)建一個(gè)底層數(shù)組再執(zhí)行添加骤视。這時(shí)結(jié)構(gòu)如下圖鞍爱,s1s2互不干擾:

[圖片上傳失敗...(image-eb729e-1622475715538)]

總結(jié)

了解了切片的底層數(shù)據(jù)結(jié)構(gòu),知道了切片傳遞的是結(jié)構(gòu)runtime.slice的值专酗,我們就能解決 90% 以上的切片問(wèn)題睹逃。再結(jié)合圖形可以很直觀的看到切片底層數(shù)據(jù)是如何操作的。

這個(gè)系列的名字是我仿造《你不知道的 JavaScript》起的??祷肯。

參考

  1. 《Go 專家編程》沉填,豆瓣鏈接:https://book.douban.com/subject/35144587/
  2. 你不知道的Go GitHub:https://github.com/darjun/you-dont-know-go

我的博客:https://darjun.github.io

歡迎關(guān)注我的微信公眾號(hào)【GoUpUp】,共同學(xué)習(xí)佑笋,一起進(jìn)步~

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末翼闹,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子蒋纬,更是在濱河造成了極大的恐慌猎荠,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,941評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件蜀备,死亡現(xiàn)場(chǎng)離奇詭異关摇,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)碾阁,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,397評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門输虱,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人脂凶,你說(shuō)我怎么就攤上這事悼瓮。” “怎么了艰猬?”我有些...
    開(kāi)封第一講書人閱讀 165,345評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)埋市。 經(jīng)常有香客問(wèn)我冠桃,道長(zhǎng),這世上最難降的妖魔是什么道宅? 我笑而不...
    開(kāi)封第一講書人閱讀 58,851評(píng)論 1 295
  • 正文 為了忘掉前任食听,我火速辦了婚禮,結(jié)果婚禮上污茵,老公的妹妹穿的比我還像新娘樱报。我一直安慰自己,他們只是感情好泞当,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,868評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布迹蛤。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪盗飒。 梳的紋絲不亂的頭發(fā)上嚷量,一...
    開(kāi)封第一講書人閱讀 51,688評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音逆趣,去河邊找鬼蝶溶。 笑死,一個(gè)胖子當(dāng)著我的面吹牛宣渗,可吹牛的內(nèi)容都是我干的抖所。 我是一名探鬼主播,決...
    沈念sama閱讀 40,414評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼痕囱,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼田轧!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起咐蝇,我...
    開(kāi)封第一講書人閱讀 39,319評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤涯鲁,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后有序,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體抹腿,經(jīng)...
    沈念sama閱讀 45,775評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,945評(píng)論 3 336
  • 正文 我和宋清朗相戀三年旭寿,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了警绩。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,096評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡盅称,死狀恐怖肩祥,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情缩膝,我是刑警寧澤混狠,帶...
    沈念sama閱讀 35,789評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站疾层,受9級(jí)特大地震影響将饺,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜痛黎,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,437評(píng)論 3 331
  • 文/蒙蒙 一予弧、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧湖饱,春花似錦掖蛤、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 31,993評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)致讥。三九已至,卻和暖如春彪置,著一層夾襖步出監(jiān)牢的瞬間拄踪,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 33,107評(píng)論 1 271
  • 我被黑心中介騙來(lái)泰國(guó)打工拳魁, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留惶桐,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,308評(píng)論 3 372
  • 正文 我出身青樓潘懊,卻偏偏與公主長(zhǎng)得像姚糊,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子授舟,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,037評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容

  • 簡(jiǎn)介 字符串(string)是 Go 語(yǔ)言提供的一種基礎(chǔ)數(shù)據(jù)類型救恨。在編程開(kāi)發(fā)中幾乎隨時(shí)都會(huì)使用。本文介紹字符串相關(guān)...
    darjun閱讀 500評(píng)論 0 0
  • array 和 slice 看似相似释树,卻有著極大的不同肠槽,但他們之間還有著千次萬(wàn)縷的聯(lián)系 slice 是引用類型、是...
    戚銀閱讀 982評(píng)論 1 4
  • 原文鏈接:https://blog.thinkeridea.com/201901/go/slice_de_yi_x...
    戚銀閱讀 1,582評(píng)論 0 0
  • Slice常見(jiàn)操作及底層原理實(shí)現(xiàn) 一 什么是Slice slice(切片)是一種數(shù)組結(jié)構(gòu)奢啥,相當(dāng)于是一個(gè)動(dòng)態(tài)的數(shù)組秸仙,...
    假程序員的世界閱讀 5,268評(píng)論 0 4
  • string string在Go語(yǔ)言內(nèi)存模型中用2字長(zhǎng)(不同CPU寂纪,字長(zhǎng)不同)的數(shù)據(jù)結(jié)構(gòu)表示,與C++ STL實(shí)現(xiàn)...
    William1125閱讀 5,598評(píng)論 0 6