Go學習筆記-切片

切片(Slice)無疑是 Go 語言中最重要的數(shù)據(jù)結構咆耿,也是最有趣的數(shù)據(jù)結構德谅,它的英文詞匯叫 slice。所有的 Go 語言開發(fā)者都津津樂道地談論切片的內(nèi)部機制萨螺,它也是 Go 語言技能面試中面試官最愛問的知識點之一窄做。初級用戶很容易濫用它,這小小的切片想要徹底的理解它是需要花費一番功夫的慰技。在使用切片之前椭盏,我覺得很有必要將切片的內(nèi)部結構做一下說明。

學過 Java 語言的人會比較容易理解切片吻商,因為它的內(nèi)部結構非常類似于 ArrayList掏颊,ArrayList 的內(nèi)部實現(xiàn)也是一個數(shù)組。
當數(shù)組容量不夠需要擴容時艾帐,就會換新的數(shù)組乌叶,還需要將老數(shù)組的內(nèi)容拷貝到新數(shù)組。
ArrayList 內(nèi)部有兩個非常重要的屬性 capacity 和 length柒爸。capacity 表示內(nèi)部數(shù)組的總長度准浴,length 表示當前已經(jīng)使用的數(shù)組的長度。length 永遠不能超過 capacity捎稚。

上圖中一個切片變量包含三個域乐横,分別是底層數(shù)組的指針、切片的長度 length 和切片的容量 capacity阳藻。切片支持 append 操作可以將新的內(nèi)容追加到底層數(shù)組晰奖,也就是填充上面的灰色格子。如果格子滿了腥泥,切片就需要擴容,底層的數(shù)組就會更換啃匿。

形象一點說蛔外,切片變量是底層數(shù)組的視圖蛆楞,底層數(shù)組是臥室,切片變量是臥室的窗戶夹厌。通過窗戶我們可以看見底層數(shù)組的一部分或全部豹爹。一個臥室可以有多個窗戶,不同的窗戶能看到臥室的不同部分矛纹。

切片的創(chuàng)建

切片的創(chuàng)建有多種方式臂聋,我們先看切片最通用的創(chuàng)建方法,那就是內(nèi)置的 make函數(shù)

package main

import "fmt"

func main() {
 var s1 []int = make([]int, 5, 8)
 var s2 []int = make([]int, 8) // 滿容切片
 fmt.Println(s1)
 fmt.Println(s2)
}

-------------
[0 0 0 0 0]
[0 0 0 0 0 0 0 0]

make 函數(shù)創(chuàng)建切片或南,需要提供三個參數(shù)孩等,分別是切片的類型、切片的長度和容量采够。其中第三個參數(shù)是可選的肄方,如果不提供第三個參數(shù),那么長度和容量相等蹬癌,也就是說切片的滿容的权她。切片和普通變量一樣,也可以使用類型自動推導逝薪,省去類型定義以及 var 關鍵字隅要。比如上面的代碼和下面的代碼是等價的。

package main

import "fmt"

func main() {
 var s1 = make([]int, 5, 8)
 s2 := make([]int, 8)
 fmt.Println(s1)
 fmt.Println(s2)
}

-------------
[0 0 0 0 0]
[0 0 0 0 0 0 0 0]

切片的初始化

使用 make 函數(shù)創(chuàng)建的切片內(nèi)容是「零值切片」董济,也就是內(nèi)部數(shù)組的元素都是零值拾徙。Go 語言還提供了另一個種創(chuàng)建切片的語法,允許我們給它賦初值感局。使用這種方式創(chuàng)建的切片是滿容的尼啡。

package main

import "fmt"

func main() {
 var s []int = []int{1,2,3,4,5}  // 滿容的
 fmt.Println(s, len(s), cap(s))
}

---------
[1 2 3 4 5] 5 5

Go 語言提供了內(nèi)置函數(shù) len() 和 cap() 可以直接獲得切片的長度和容量屬性。

空切片

在創(chuàng)建切片時询微,還有兩個非常特殊的情況需要考慮崖瞭,那就是容量和長度都是零的切片,叫著「空切片」撑毛,這個不同于前面說的「零值切片」书聚。

package main

import "fmt"

func main() {
 var s1 []int
 var s2 []int = []int{}
 var s2 []int = make([]int, 0)
 fmt.Println(s1, s2, s3)
 fmt.Println(len(s1), len(s2), len(s3))
 fmt.Println(cap(s1), cap(s2), cap(s3))
}

-----------
[] [] []
0 0 0
0 0 0

上面三種形式創(chuàng)建的切片都是「空切片」,不過在內(nèi)部結構上這三種形式是有差異的藻雌,甚至第一種都不叫「空切片」雌续,而是叫著「 nil 切片」。但是在形式上它們幾乎一摸一樣胯杭,用起來差不多沒有區(qū)別驯杜。所以初級用戶可以不必區(qū)分「空切片」和「 nil 切片」,到后續(xù)章節(jié)我們會仔細分析這兩種形式的區(qū)別做个。

切片的賦值

切片的賦值是一次淺拷貝操作鸽心,拷貝的是切片變量的三個域滚局,你可以將切片變量看成長度為 3 的 int 型數(shù)組,數(shù)組的賦值就是淺拷貝顽频√僦拷貝前后兩個變量共享底層數(shù)組,對一個切片的修改會影響另一個切片的內(nèi)容糯景,這點需要特別注意嘁圈。

package main

import "fmt"

func main() {
 var s1 = make([]int, 5, 8)
 // 切片的訪問和數(shù)組差不多
 for i := 0; i < len(s1); i++ {
  s1[i] = i + 1
 }
 var s2 = s1
 fmt.Println(s1, len(s1), cap(s1))
 fmt.Println(s2, len(s2), cap(s2))

 // 嘗試修改切片內(nèi)容
 s2[0] = 255
 fmt.Println(s1)
 fmt.Println(s2)
}

--------
[1 2 3 4 5] 5 8
[1 2 3 4 5] 5 8
[255 2 3 4 5]
[255 2 3 4 5]

從上面的輸出中可以看到賦值的兩切片共享了底層數(shù)組。

切片的遍歷

切片在遍歷的語法上和數(shù)組是一樣的蟀淮,除了支持下標遍歷外最住,那就是使用 range 關鍵字

package main


import "fmt"


func main() {
    var s = []int{1,2,3,4,5}
    for index := range s {
        fmt.Println(index, s[index])
    }
    for index, value := range s {
        fmt.Println(index, value)
    }
}

--------
0 1
1 2
2 3
3 4
4 5
0 1
1 2
2 3
3 4
4 5

切片的追加

文章開頭提到切片是動態(tài)的數(shù)組,其長度是可以變化的灭贷。什么操作可以改變切片的長度呢温学,這個操作就是追加操作。切片每一次追加后都會形成新的切片變量甚疟,如果底層數(shù)組沒有擴容仗岖,那么追加前后的兩個切片變量共享底層數(shù)組,如果底層數(shù)組擴容了览妖,那么追加前后的底層數(shù)組是分離的不共享的轧拄。如果底層數(shù)組是共享的,一個切片的內(nèi)容變化就會影響到另一個切片讽膏,這點需要特別注意檩电。

package main

import "fmt"

func main() {
 var s1 = []int{1,2,3,4,5}
 fmt.Println(s1, len(s1), cap(s1))

 // 對滿容的切片進行追加會分離底層數(shù)組
 var s2 = append(s1, 6)
 fmt.Println(s1, len(s1), cap(s1))
 fmt.Println(s2, len(s2), cap(s2))

 // 對非滿容的切片進行追加會共享底層數(shù)組
 var s3 = append(s2, 7)
 fmt.Println(s2, len(s2), cap(s2))
 fmt.Println(s3, len(s3), cap(s3))
}

--------------------------
[1 2 3 4 5] 5 5
[1 2 3 4 5] 5 5
[1 2 3 4 5 6] 6 10
[1 2 3 4 5 6] 6 10
[1 2 3 4 5 6 7] 7 10

正是因為切片追加后是新的切片變量,Go 編譯器禁止追加了切片后不使用這個新的切片變量府树,以避免用戶以為追加操作的返回值和原切片變量是同一個變量俐末。

package main

import "fmt"

func main() {
 var s1 = []int{1,2,3,4,5}
 append(s1, 6)
 fmt.Println(s1)
}

--------------
./main.go:7:8: append(s1, 6) evaluated but not used

如果你真的不需要使用這個新的變量,可以將 append 的結果賦值給下劃線變量奄侠。下劃線變量是 Go 語言特殊的內(nèi)置變量卓箫,它就像一個黑洞,可以將任意變量賦值給它垄潮,但是卻不能讀取這個特殊變量滥崩。

package main

import "fmt"

func main() {
 var s1 = []int{1,2,3,4,5}
 _ = append(s1, 6)
 fmt.Println(s1)
}

----------
[1 2 3 4 5]

還需要注意的是追加雖然會導致底層數(shù)組發(fā)生擴容怕磨,更換的新的數(shù)組倍宾,但是舊數(shù)組并不會立即被銷毀被回收赁严,因為老切片還指向這舊數(shù)組。

切片的域是只讀的

我們剛才說切片的長度是可以變化的牡整,為什么又說切片是只讀的呢藐吮?這不是矛盾么。這是為了提醒讀者注意切片追加后形成了一個新的切片變量,而老的切片變量的三個域其實并不會改變炎码,改變的只是底層的數(shù)組盟迟。這里說的是切片的「域」是只讀的秋泳,而不是說切片是只讀的潦闲。切片的「域」就是組成切片變量的三個部分,分別是底層數(shù)組的指針迫皱、切片的長度和切片的容量歉闰。這里讀者需要仔細咀嚼。

切割切割

到目前位置還沒有說明切片名字的由來卓起,既然叫著切片和敬,那總得可以切割吧。切割切割戏阅,有些男娃子聽到這個詞匯時身上會起雞皮疙瘩昼弟。切片的切割可以類比字符串的子串,它并不是要把切片割斷奕筐,而是從母切片中拷貝出一個子切片來舱痘,子切片和母切片共享底層數(shù)組。下面我們來看一下切片究竟是如何切割的离赫。

package main

import "fmt"

func main() {
 var s1 = []int{1,2,3,4,5,6,7}
 // start_index 和 end_index芭逝,不包含 end_index
 // [start_index, end_index)
 var s2 = s1[2:5] 
 fmt.Println(s1, len(s1), cap(s1))
 fmt.Println(s2, len(s2), cap(s2))
}

------------
[1 2 3 4 5 6 7] 7 7
[3 4 5] 3 5

上面的輸出需要特別注意的是,既然切割前后共享底層數(shù)組渊胸,那為什么容量不一樣呢旬盯?解釋它我必須要畫圖了,讀者請務必仔細觀察下面這張圖

我們注意到子切片的內(nèi)部數(shù)據(jù)指針指向了數(shù)組的中間位置翎猛,而不再是數(shù)組的開頭了胖翰。子切片容量的大小是從中間的位置開始直到切片末尾的長度,母子切片依舊共享底層數(shù)組切厘。

子切片語法上要提供起始和結束位置萨咳,這兩個位置都可選的,不提供起始位置迂卢,默認就是從母切片的初始位置開始(不是底層數(shù)組的初始位置)某弦,不提供結束位置,默認就結束到母切片尾部(是長度線而克,不是容量線)靶壮。下面我們看個例子:

package main

import "fmt"

func main() {
 var s1 = []int{1, 2, 3, 4, 5, 6, 7}
 var s2 = s1[:5]
 var s3 = s1[3:]
 var s4 = s1[:]
 fmt.Println(s1, len(s1), cap(s1))
 fmt.Println(s2, len(s2), cap(s2))
 fmt.Println(s3, len(s3), cap(s3))
 fmt.Println(s4, len(s4), cap(s4))
}

-----------
[1 2 3 4 5 6 7] 7 7
[1 2 3 4 5] 5 7
[4 5 6 7] 4 4
[1 2 3 4 5 6 7] 7 7

細心的同學可能會注意到上面的 s1[:] 很特別,它和普通的切片賦值有區(qū)別么员萍?答案是沒區(qū)別腾降,這非常讓人感到意外,同樣的共享底層數(shù)組碎绎,同樣是淺拷貝螃壤。下面我們來驗證一下

package main

import "fmt"

func main() {
 var s = make([]int, 5, 8)
 for i:=0;i<len(s);i++ {
  s[i] = i+1
 }
 fmt.Println(s, len(s), cap(s))

 var s2 = s
 var s3 = s[:]
 fmt.Println(s2, len(s2), cap(s2))
 fmt.Println(s3, len(s3), cap(s3))

 // 修改母切片
 s[0] = 255
 fmt.Println(s, len(s), cap(s))
 fmt.Println(s2, len(s2), cap(s2))
 fmt.Println(s3, len(s3), cap(s3))
}

-------------
[1 2 3 4 5] 5 8
[1 2 3 4 5] 5 8
[1 2 3 4 5] 5 8
[255 2 3 4 5] 5 8
[255 2 3 4 5] 5 8
[255 2 3 4 5] 5 8

使用過 Python 的同學可能會問抗果,切片支持負數(shù)的位置么,答案是不支持奸晴,下標不可以是負數(shù)冤馏。

數(shù)組變切片

對數(shù)組進行切割可以轉換成切片,切片將原數(shù)組作為內(nèi)部底層數(shù)組寄啼。也就是說修改了原數(shù)組會影響到新切片逮光,對切片的修改也會影響到原數(shù)組。

package main

import "fmt"

func main() {
    var a = [10]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
    var b = a[2:6]
    fmt.Println(b)
    a[4] = 100
    fmt.Println(b)
}

-------
[3 4 5 6]
[3 4 100 6]

copy 函數(shù)

Go 語言還內(nèi)置了一個 copy 函數(shù)墩划,用來進行切片的深拷貝涕刚。不過其實也沒那么深,只是深到底層的數(shù)組而已乙帮。如果數(shù)組里面裝的是指針杜漠,比如 []*int 類型,那么指針指向的內(nèi)容還是共享的察净。

func copy(dst, src []T) int

copy 函數(shù)不會因為原切片和目標切片的長度問題而額外分配底層數(shù)組的內(nèi)存驾茴,它只負責拷貝數(shù)組的內(nèi)容,從原切片拷貝到目標切片塞绿,拷貝的量是原切片和目標切片長度的較小值 —— min(len(src), len(dst))沟涨,函數(shù)返回的是拷貝的實際長度。我們來看一個例子

package main

import "fmt"

func main() {
 var s = make([]int, 5, 8)
 for i:=0;i<len(s);i++ {
  s[i] = i+1
 }
 fmt.Println(s)
 var d = make([]int, 2, 6)
 var n = copy(d, s)
 fmt.Println(n, d)
}
-----------
[1 2 3 4 5]
2 [1 2]

切片的擴容點

當比較短的切片擴容時异吻,系統(tǒng)會多分配 100% 的空間裹赴,也就是說分配的數(shù)組容量是切片長度的2倍。但切片長度超過1024時诀浪,擴容策略調(diào)整為多分配 25% 的空間棋返,這是為了避免空間的過多浪費。試試解釋下面的運行結果雷猪。

s1 := make([]int, 6)
s2 := make([]int, 1024)
s1 = append(s1, 1)
s2 = append(s2, 2)
fmt.Println(len(s1), cap(s1))
fmt.Println(len(s2), cap(s2))
-------------------------------------------
7 12
1025 1344
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末睛竣,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子求摇,更是在濱河造成了極大的恐慌射沟,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,607評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件与境,死亡現(xiàn)場離奇詭異验夯,居然都是意外死亡,警方通過查閱死者的電腦和手機摔刁,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,239評論 3 395
  • 文/潘曉璐 我一進店門挥转,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事绑谣〉炒埽” “怎么了?”我有些...
    開封第一講書人閱讀 164,960評論 0 355
  • 文/不壞的土叔 我叫張陵借宵,是天一觀的道長幌衣。 經(jīng)常有香客問我,道長暇务,這世上最難降的妖魔是什么泼掠? 我笑而不...
    開封第一講書人閱讀 58,750評論 1 294
  • 正文 為了忘掉前任怔软,我火速辦了婚禮垦细,結果婚禮上,老公的妹妹穿的比我還像新娘挡逼。我一直安慰自己括改,他們只是感情好,可當我...
    茶點故事閱讀 67,764評論 6 392
  • 文/花漫 我一把揭開白布家坎。 她就那樣靜靜地躺著嘱能,像睡著了一般。 火紅的嫁衣襯著肌膚如雪虱疏。 梳的紋絲不亂的頭發(fā)上惹骂,一...
    開封第一講書人閱讀 51,604評論 1 305
  • 那天,我揣著相機與錄音做瞪,去河邊找鬼对粪。 笑死,一個胖子當著我的面吹牛装蓬,可吹牛的內(nèi)容都是我干的著拭。 我是一名探鬼主播,決...
    沈念sama閱讀 40,347評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼牍帚,長吁一口氣:“原來是場噩夢啊……” “哼儡遮!你這毒婦竟也來了?” 一聲冷哼從身側響起暗赶,我...
    開封第一講書人閱讀 39,253評論 0 276
  • 序言:老撾萬榮一對情侶失蹤鄙币,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后蹂随,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體十嘿,經(jīng)...
    沈念sama閱讀 45,702評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,893評論 3 336
  • 正文 我和宋清朗相戀三年糙及,在試婚紗的時候發(fā)現(xiàn)自己被綠了详幽。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,015評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖唇聘,靈堂內(nèi)的尸體忽然破棺而出版姑,到底是詐尸還是另有隱情,我是刑警寧澤迟郎,帶...
    沈念sama閱讀 35,734評論 5 346
  • 正文 年R本政府宣布剥险,位于F島的核電站,受9級特大地震影響宪肖,放射性物質發(fā)生泄漏表制。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,352評論 3 330
  • 文/蒙蒙 一控乾、第九天 我趴在偏房一處隱蔽的房頂上張望么介。 院中可真熱鬧,春花似錦蜕衡、人聲如沸慨仿。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,934評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽帘撰。三九已至,卻和暖如春万皿,著一層夾襖步出監(jiān)牢的瞬間摧找,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,052評論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留赃承,地道東北人怕品。 一個月前我還...
    沈念sama閱讀 48,216評論 3 371
  • 正文 我出身青樓肉康,卻偏偏與公主長得像闯估,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子吼和,可洞房花燭夜當晚...
    茶點故事閱讀 44,969評論 2 355

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

  • 切片(slice)是 Golang 中一種比較特殊的數(shù)據(jù)結構涨薪,這種數(shù)據(jù)結構更便于使用和管理數(shù)據(jù)集合。切片是圍繞動態(tài)...
    小孩真笨閱讀 1,074評論 0 1
  • 一、數(shù)組 《快學 Go 語言》第 4 課 —— 低調(diào)的數(shù)組Go 語言里面的數(shù)組其實很不常用厢岂,這是因為數(shù)組是定長的靜...
    合肥黑閱讀 968評論 0 2
  • 切片(slice)是 Golang 中一種比較特殊的數(shù)據(jù)結構,這種數(shù)據(jù)結構更便于使用和管理數(shù)據(jù)集合阳距。切片是圍繞動態(tài)...
    51reboot閱讀 28,649評論 2 10
  • 數(shù)組 和C語言一樣,Go語言中也有數(shù)組的概念, Go語言中的數(shù)組也是用于保存一組相同類型的數(shù)據(jù) 和C語言一樣,Go...
    極客江南閱讀 1,208評論 0 2
  • 此處數(shù)組只講Go語言中和C語言不一樣的地方 格式不同:Go語言定義數(shù)組的格式:var ages [3]int 定義...
    AuglyXu閱讀 1,348評論 0 0