本文翻譯自Rob Pike的文章《Arrays, slices (and strings): The mechanics of 'append'》谆构,原文地址 https://blog.golang.org/slices祈坠。
原博文中出了一些練習(xí)柑爸,譯者提出了自己的解法蜓斧,原文中并不包含這些解法魁莉。
前言
數(shù)組是面向過(guò)程的的編程語(yǔ)言最常見的特性之一子库。
數(shù)組看起來(lái)很簡(jiǎn)單舱权,不過(guò)要在語(yǔ)言中實(shí)現(xiàn)這一特性往往要面臨很多問(wèn)題,比如:
- 數(shù)組應(yīng)該是固定長(zhǎng)度還是是可變長(zhǎng)度仑嗅?
- 長(zhǎng)度應(yīng)該是數(shù)組類型的屬性嗎宴倍?
- 多維數(shù)組應(yīng)該是什么樣子?
- 如何定義空數(shù)組仓技?
對(duì)上面問(wèn)題的解答鸵贬,關(guān)系著數(shù)組是否只是一個(gè)feature還是語(yǔ)言設(shè)計(jì)的核心。
在Go的早期開發(fā)中脖捻,花了大約一年時(shí)間來(lái)回答上面的問(wèn)題阔逼。切片的引入是解決問(wèn)題的關(guān)鍵,它建立在固定大小的數(shù)組上地沮,提供了靈活嗜浮,可擴(kuò)展的數(shù)據(jù)結(jié)構(gòu)。但是時(shí)至今日摩疑,Go語(yǔ)言新手經(jīng)常在切片的使用上犯錯(cuò)危融,這可能與他們受過(guò)其他語(yǔ)言的影響有關(guān)。
在這篇博文中未荒,我們將嘗試通過(guò)解釋內(nèi)置append函數(shù)的工作原理专挪,以及這個(gè)函數(shù)的設(shè)計(jì)思想,來(lái)幫助新手消除這些誤解片排。
數(shù)組
數(shù)組是Go語(yǔ)言中的一個(gè)重要組成部分寨腔,但就像建筑的地基一樣,它不容易讓使用者覺察到率寡。在深入了解切片之前迫卢,我們必須對(duì)數(shù)組有一個(gè)簡(jiǎn)單的了解。
在Go程序中冶共,數(shù)組并不常見乾蛤,因?yàn)殚L(zhǎng)度是數(shù)組類型的一部分每界,而這一點(diǎn)限制了數(shù)組的表達(dá)能力。
下面語(yǔ)句
var buffer [256]byte
聲明了一個(gè)占用256個(gè)byte
家卖,名為buffer
的變量眨层,buffer
的類型中包含了它的長(zhǎng)度,[256]byte
上荡。占用512個(gè)byte
的數(shù)組的類型將是[512]byte
趴樱。
buffer在內(nèi)存中的表示就像下面這樣
buffer: byte byte byte ... 重復(fù)256次 ... byte byte byte
buffer包含了256個(gè)字節(jié)的數(shù)據(jù)。我們可以通過(guò)常見索引的方式來(lái)訪問(wèn)這個(gè)數(shù)據(jù)的元素酪捡,buffer[0]
,buffer[1]
直到buffer[255]
叁征。訪問(wèn)超過(guò)索引的元素將會(huì)導(dǎo)致程序崩潰。
內(nèi)置的len
函數(shù)能返回?cái)?shù)組和其他幾種類型的長(zhǎng)度逛薇。對(duì)數(shù)據(jù)來(lái)說(shuō)捺疼,len
函數(shù)的返回時(shí)顯而易見的,比如len(buffer)
會(huì)返回256永罚。
數(shù)組時(shí)很有用的啤呼,它可以用來(lái)表示轉(zhuǎn)換矩陣。但是在Go語(yǔ)言中尤蛮,數(shù)組最常見的作用是存儲(chǔ)切片的數(shù)據(jù)媳友。
切片
切片頭
切片是本文討論的重點(diǎn)斯议,要想合理地使用它产捞,必須先理解它的工作方式。
切片是描述與切片變量本身分開存儲(chǔ)的數(shù)組的連續(xù)部分的數(shù)據(jù)結(jié)構(gòu)哼御。切片不是數(shù)組坯临。切片描述了數(shù)組的一部分。
我們可以通過(guò)下面的方式創(chuàng)建一個(gè)切片恋昼,這個(gè)切片描述了buffer
數(shù)組第100(閉區(qū)間)到150(開區(qū)間)之間的元素
var slice []byte = buffer[100:150]
變量slice
的類型是[]byte
看靠,它從buffer
數(shù)組中初始化。更加通用的初始化語(yǔ)句如下:
var slice = buffer[100:150]
在函數(shù)內(nèi)部可以定義的更簡(jiǎn)短些
slice := buffer[100:150]
這個(gè)slice
變量到底是什么呢液肌?現(xiàn)在設(shè)想切片是一個(gè)包含兩個(gè)元素的的數(shù)據(jù)結(jié)構(gòu):長(zhǎng)度和指向數(shù)組某個(gè)元素的指針挟炬。可以認(rèn)為就像下面的結(jié)構(gòu)體一樣
type sliceHeader struct {
Length int
ZerothElement *byte
}
slice := sliceHeader {
Length: 50
ZeroElement &buffer[100],
}
當(dāng)然嗦哆,真正的實(shí)現(xiàn)肯定不是這樣的谤祖。盡管sliceHeader
對(duì)程序員是不可見的,并且指針和指向的元素類型有關(guān)老速,但是上面的代碼展示了實(shí)現(xiàn)的正確思路粥喜。
之前我們對(duì)一個(gè)數(shù)據(jù)進(jìn)行了切片操作,其實(shí)我們也可以對(duì)一個(gè)切片進(jìn)行切片橘券,比如下面這樣
slice2 := slice[5:10]
和之前一樣额湘,這行代碼創(chuàng)建了一個(gè)新的切片卿吐,指向原來(lái)切片的5到9(閉)之間的元素,也就是說(shuō)指向了buffer
數(shù)組的105到109之間的元素锋华。slice2
的sliceHeader
結(jié)構(gòu)就像下面這樣
slice := sliceHeader{
Length: 5,
ZerothElement &buffer[105],
}
我們也可以執(zhí)行reslice操作嗡官,即對(duì)一個(gè)切片進(jìn)行切片操作,并把返回值傳給之前的切片
slice = slice[5:10]
這時(shí)slice
就和slice2
的結(jié)構(gòu)一樣了毯焕。reslice
操作對(duì)截?cái)嘁粋€(gè)切片是很有用的谨湘,比如下面的代碼中截掉了第一個(gè)和最后一個(gè)元素
slice = slice[1:len(slice)-1]
你能經(jīng)常聽到有經(jīng)驗(yàn)的Go程序員談?wù)?code>slice header,因?yàn)檫@就是切片變量存儲(chǔ)的形式芥丧。舉個(gè)例子紧阔,當(dāng)你調(diào)用一個(gè)傳入一個(gè)切片作為參數(shù)的函數(shù),比如bytes.IndexRune
续担,實(shí)際上傳入的就是一個(gè)slice header
slashPos := bytes.IndexRune(slice, '/')
練習(xí):寫出經(jīng)過(guò)上面操作后slice
變量的sliceHeader
slice := sliceHeader{
Length: 3,
ZerothElement &buffer[106],
}
將切片作為參數(shù)傳入
有必要知道擅耽,即使切片包含了一個(gè)指針,它本身也是有值的物遇,它是一個(gè)包含一個(gè)指針和一個(gè)長(zhǎng)度數(shù)值的結(jié)構(gòu)體實(shí)例乖仇,并不是一個(gè)指向這個(gè)結(jié)構(gòu)體實(shí)例的指針。
在上面的例子中询兴,indexRune
函數(shù)傳入了一個(gè)slice header的拷貝乃沙。這很重要。
考慮下面的函數(shù)诗舰。
func AddOneToEachElement(slice []byte){
for i:= range slice {
slice[i++]
}
}
函數(shù)功能就是遍歷切片中的元素警儒,每個(gè)元素加1.
實(shí)際跑跑看:
func main(){
slice := buffer[10:20]
for i := 0; i < len(slice); i++ {
slice[i] = byte(i)
}
fmt.Println("before", slice)
AddOneToEachElement(slice)
fmt.Println("after", slice)
}
// before [0 1 2 3 4 5 6 7 8 9]
// after [1 2 3 4 5 6 7 8 9 10]
盡管slice header是通過(guò)傳值的方式傳給函數(shù),但是因?yàn)閔eader中包含了指向某個(gè)數(shù)組的指針眶根,所以原始的header和作為參數(shù)傳入函數(shù)header的拷貝其實(shí)描述的都是同一個(gè)數(shù)組蜀铲。因此當(dāng)函數(shù)返回時(shí),可以通過(guò)原始slice變量查看修改后的元素属百。
傳入函數(shù)的參數(shù)實(shí)際上是一個(gè)復(fù)制记劝,比如下面的例子:
func SubtractOneFromLength(slice []byte) []byte {
slice = slice[0 : len(slice)-1]
return slice
}
func main() {
fmt.Println("Before: len(slice) =", len(slice))
newSlice := SubtractOneFromLength(slice)
fmt.Println("After: len(slice)=", len(slice))
fmt.Println("After: len(newSlice) =", len(newSlice))
}
// Before: len(slice) = 50
// After: len(slice) = 50
// After: len(newSlice) = 49
我們可以看到切片指向的內(nèi)容能夠被函數(shù)改變,而切片的header不能被函數(shù)改變族扰。slice變量中長(zhǎng)度屬性不會(huì)被函數(shù)修改厌丑,因?yàn)閭魅牒瘮?shù)的是切片header的一份拷貝,并不是header本身渔呵。因此怒竿。如果想寫一個(gè)函數(shù)修改header,就必須將修改后的header作為結(jié)果返回厘肮。在上面的例子中愧口,slice
變量本身并沒(méi)有被改變,但是返回的切片有了新的長(zhǎng)度类茂,這個(gè)返回存在了newSlice
中耍属。
指向切片的指針
另外一種可以在函數(shù)中改變切片header的方式就是傳入指向切片的指針托嚣。下面是一個(gè)例子。
func PtrSubtractOneFromLength(slicePtr *[]byte) {
slice := *slicePtr
*slicePtr = slice[0 : len(slice)-1]
}
func main() {
fmt.Println("Before: len(slice) =", len(slice))
PtrSubtractOneFromLength(&slice)
fmt.Println("After: len(slice) =", len(slice))
}
// Before: len(slice) = 50
// After: len(slice) = 49
上面的例子看起來(lái)有點(diǎn)傻厚骗,特別是增加了一個(gè)臨時(shí)變量來(lái)改變切片的長(zhǎng)度示启。處理切片指針是很常見的情況,通常领舰,在修改切片的函數(shù)中夫嗓,都會(huì)使用一個(gè)指針接收器。
假設(shè)我們想實(shí)現(xiàn)一個(gè)方法冲秽,截?cái)嗲衅凶詈笠粋€(gè)'/'及其后面的元素舍咖,可以這樣寫
type path []byte
func (p *path) TruncateAtFinalSlash() {
i := bytes.LastIndex(*p, []byte("/"))
if i >= 0 {
*p = (*p)[0:i]
}
}
func main() {
pathName := path("/usr/bin/tso")
pathName.TruncateAtFinalSlash()
fmt.Println("%s\n", pathName)
}
// /usr/bin
練習(xí):將接收器的類型由指針修改為值,然后再跑一遍
修改后的函數(shù)為
func (p path) TruncateAtFinalSlash() {
i := bytes.LastIndex(p, []byte("/"))
if i >= 0 {
p = p[0:i]
}
}
// /usr/bin/tso
// 因?yàn)閭魅氲氖莗athName的拷貝锉桑,main函數(shù)中的pathName的長(zhǎng)度并沒(méi)有發(fā)生改變
另一方面排霉,如果想實(shí)現(xiàn)一個(gè)方法,將path
中的ASCII值變?yōu)榇髮懨裰幔敲纯梢詡魅胍粋€(gè)值攻柠,因?yàn)閭魅氲闹狄廊粫?huì)指向同一個(gè)數(shù)組。
type path []byte
func (p path) ToUpper(){
for i, b := range p {
if 'a' <= b && b <= 'z' {
p[i] = b + 'A' - 'a'
}
}
}
func main() {
pathName := path("/usr/bin/tso")
pathName.ToUpper()
fmt.Println("%s\n", pathName)
}
// /USR/BIN/TSO
練習(xí): 修改ToUpper
方法后裸,使用指針接收器瑰钮,看看結(jié)果會(huì)不會(huì)有變化
func (p *path) ToUpper() {
for i, b := range *p {
if 'a' <= b && b <= 'z' {
(*p)[i] = b + 'A' - 'a'
}
}
}
// /USR/BIN/TSO
結(jié)果沒(méi)有變化
容量
下面這個(gè)函數(shù)每次都會(huì)為一個(gè)切片添加一個(gè)元素。
func Extend(slice []int, element int) []int {
n := len(slice)
slice = slice[0 : n+1]
slice[n] = element
return slice
}
func main() {
var iBuffer [10]int
slice := iBuffer[0:0]
for i := 0; i < 20; i++ {
slice = Extend(slice, i)
fmt.Println(slice)
}
}
// [0]
// [0 1]
// [0 1 2]
// [0 1 2 3]
// [0 1 2 3 4]
// [0 1 2 3 4 5]
// [0 1 2 3 4 5 6]
// [0 1 2 3 4 5 6 7]
// [0 1 2 3 4 5 6 7 8]
// [0 1 2 3 4 5 6 7 8 9]
// panic: runtime error: slice bounds out of range
// goroutine 1 [running]:
// panic(0x1022c0, 0x1040a018)
// /usr/local/go/src/runtime/panic.go:500 +0x720
// main.main()
// /tmp/sandbox219409371/main.go:27 +0x1e0
現(xiàn)在該聊一聊slice header的第三個(gè)屬性--容量了微驶。除了數(shù)組指針和長(zhǎng)度浪谴,slice header也存儲(chǔ)了容量。
type sliceHeader struct {
Length int
Capacity int
ZerothElement *byte
}
Capacity屬性記錄了切片指向的數(shù)組實(shí)際占有的空間祈搜,它是Length
的最大值较店。如果切片的長(zhǎng)度超過(guò)了容量,會(huì)導(dǎo)致程序的崩潰容燕。
在執(zhí)行下面的語(yǔ)句后
slice := iBuffer[0:0]
slice的header結(jié)構(gòu)如下
slice := sliceHeader{
Length: 0,
Capacity: 10,
ZerothElement: &iBuffer[0],
}
屬性Capacity
的值為指向數(shù)組的長(zhǎng)度減去切片指向的第一個(gè)元素在數(shù)組中的索引值』槎龋可以使用內(nèi)置的cap
函數(shù)獲取切片的容量蘸秘。
if cap(slice) == len(slice) {
fmt.Println("slice is full!")
}
Make方法
如果想要擴(kuò)展切片使其大于本身的容量呢?不可能蝗茁!容量一定是切片大小所能達(dá)到的極限值醋虏。但是可以通過(guò)創(chuàng)建一個(gè)新的數(shù)組,將原來(lái)數(shù)組中的元素復(fù)制過(guò)去哮翘,然后讓切片來(lái)描述這個(gè)新數(shù)組颈嚼。
我們可以使用內(nèi)置的new
函數(shù)來(lái)生成一個(gè)更大的數(shù)組,然后對(duì)其切片饭寺,但是使用內(nèi)置的make
函數(shù)會(huì)更簡(jiǎn)單一些阻课。make
函數(shù)會(huì)創(chuàng)建一個(gè)新的數(shù)組的同時(shí)叫挟,創(chuàng)建一個(gè)切片來(lái)描述這個(gè)數(shù)組。make
函數(shù)接受三個(gè)參數(shù):切片的類型限煞,長(zhǎng)度抹恳,容量,容量也就是創(chuàng)建的數(shù)組的大小署驻。
下面的代碼會(huì)創(chuàng)建一個(gè)長(zhǎng)度為10奋献,容量為15的切片。
slice := make([]int, 10, 15)
fmt.Println("len %d, cap: %d\n", len(slice), cap(slice))
// len: 10, cap: 15
下面的代碼會(huì)讓slice
的容量加倍旺上,但是長(zhǎng)度不變
slice := make([]int, 10 ,15)
fmt.Println("len: %d, cap: %d\n", len(slice), cap(slice))
newSlice := make([]int, len(slice), 2*cap(slice))
for i := range slice {
newSlice[i] = slice[i]
}
slice = newSlice
fmt.Println("len: %d, cap: %d", len(slice), cap(slice))
// len: 10, cap: 15
// len: 10, cap: 30
當(dāng)創(chuàng)建切片時(shí)瓶蚂,經(jīng)常需要長(zhǎng)度和容量一致,在這種情況下宣吱,可以不傳入容量扬跋,容量值默認(rèn)為長(zhǎng)度值。
gophers := make([]Gopher, 10)
// gophers 的容量和長(zhǎng)度都為10
Copy
在上面的例子中凌节,當(dāng)容量增倍時(shí)钦听,使用了一個(gè)循環(huán)將舊切片中的值,傳給了新的切片倍奢。Go有一個(gè)內(nèi)置copy
函數(shù)來(lái)簡(jiǎn)化這一操作朴上。copy
函數(shù)接受兩個(gè)切片,將右邊切片的內(nèi)容復(fù)制給左邊的切片卒煞。示例代碼如下
newSlice := make([]int, len(slice), 2*cap(slice))
copy(newSlice, slice)
copy方法是很智能的痪宰,它會(huì)關(guān)注兩邊切片的長(zhǎng)度,復(fù)制能復(fù)制的部分畔裕。換句話說(shuō)衣撬,它復(fù)制的元素個(gè)數(shù)等于兩個(gè)切片長(zhǎng)度的較小值。這能幫助使用者省不少事扮饶。copy
函數(shù)會(huì)返回復(fù)制的元素個(gè)數(shù)具练,盡管有時(shí)候并沒(méi)有必要進(jìn)行相應(yīng)的檢查。
當(dāng)源切片和目的切片在內(nèi)容上有重合時(shí)甜无,copy
也能正確工作扛点,也就是說(shuō)我們可以使用copy
來(lái)進(jìn)行移位操作。下面是一個(gè)使用copy
來(lái)在切片中間插入一個(gè)元素的例子岂丘。
// 在指定的index處插入一個(gè)值
// 這個(gè)index必須在容量范圍內(nèi)
func Insert(slice []int, index, value int) []int {
slice = slice[0 : len(slice)+1]
copy(slice[index+1:], slice[index:])
slice[index] = value
return slice
}
上面的函數(shù)中有一些需要注意的陵究。首先,它返回了一個(gè)長(zhǎng)度被改變的切片奥帘;其次铜邮,上面例子中使用了一個(gè)簡(jiǎn)寫。表達(dá)式
slice[i:]
和表達(dá)式
slice[i:len(slice)]
產(chǎn)生的效果是一樣的。同樣的松蒜,也可以把冒號(hào)左邊的值留空扔茅,它默認(rèn)為0。
所以slice[:]
就是他本身牍鞠。
現(xiàn)在將Insert
函數(shù)跑起來(lái)
slice := make([]int, 10, 20)
for i := range slice {
slice[i] = i
}
fmt.Println(slice)
slice = Insert(slice, 5, 99)
fmt.Println(slice)
// [0 1 2 3 4 5 6 7 8 9]
// [0 1 2 3 4 99 5 6 7 8 9]
Append
上面的例子中咖摹,我們寫一個(gè)Extend
函數(shù)將一個(gè)元素添加到切片。這個(gè)函數(shù)是有bug的难述,當(dāng)切片的容量太小時(shí)萤晴,程序會(huì)崩潰掉(Insert
函數(shù)也有同樣的問(wèn)題)。現(xiàn)在來(lái)寫一個(gè)更加健壯的函數(shù)來(lái)實(shí)現(xiàn)向[]int
類型的切片添加元素的功能
func Extend(slice []int, element int) []int {
n := len(slice)
if n == cap(slice) {
newSlice := make([]int, len(slice), 2*len(slice)+1)
copy(newSlice, slice)
slice = newSlice
}
slice = slice[0 : n+1]
slice[n] = element
return slice
}
上面的函數(shù)重新分配了一個(gè)數(shù)組胁后,所以必須將新生成切片返回店读。下面的代碼會(huì)調(diào)用Extend
函數(shù)
slice := make([]int, 0, 5)
for i:= 0; i < 10; i++ {
slice = Extend(slice, i)
fmt.Printf("len=%d cap=%d slice=%v\n", len(slice), cap(slice), slice)
fmt.Println("address of 0th element:", &slice[0])
}
// len=1 cap=5 slice=[0]
// address of 0th element: 0x10432200
// len=2 cap=5 slice=[0 1]
// address of 0th element: 0x10432200
// len=3 cap=5 slice=[0 1 2]
// address of 0th element: 0x10432200
// len=4 cap=5 slice=[0 1 2 3]
// address of 0th element: 0x10432200
// len=5 cap=5 slice=[0 1 2 3 4]
// address of 0th element: 0x10432200
// len=6 cap=11 slice=[0 1 2 3 4 5]
// address of 0th element: 0x10436120
// len=7 cap=11 slice=[0 1 2 3 4 5 6]
// address of 0th element: 0x10436120
// len=8 cap=11 slice=[0 1 2 3 4 5 6 7]
// address of 0th element: 0x10436120
// len=9 cap=11 slice=[0 1 2 3 4 5 6 7 8]
// address of 0th element: 0x10436120
// len=10 cap=11 slice=[0 1 2 3 4 5 6 7 8 9]
// address of 0th element: 0x10436120
可以看到,在初始的容量5被占滿后攀芯,新切片的容量屯断,和指向的第一個(gè)元素的地址都變化了。
繼承上面Extend
函數(shù)的思路侣诺,我們甚至可以實(shí)現(xiàn)一個(gè)方法來(lái)在切片添加多個(gè)元素殖演。要實(shí)現(xiàn)這樣的功能,需要用到Go語(yǔ)言講參數(shù)列表轉(zhuǎn)換為切片的特性年鸳。也就是Go語(yǔ)言的可變參數(shù)特性趴久。
新的函數(shù)叫做Append
。在第一個(gè)版本中搔确,我們直接多次調(diào)用Extend
來(lái)實(shí)現(xiàn)相應(yīng)的功能彼棍。Append
函數(shù)的聲明如下:
func Append(slice []int, items ...int) []int
實(shí)現(xiàn)如下
func Append(slice []int, items ...int) []int {
for _, item := range items {
slice = Extend(slice, item)
}
}
實(shí)際跑一跑
slice := []int{0, 1, 2, 3, 4}
fmt.Println(slice)
slice = Append(slice, 5, 6, 7, 8)
fmt.Println(slice)
// [0 1 2 3 4]
// [0 1 2 3 4 5 6 7 8]
Append函數(shù)還有另外一個(gè)有意思的特性。我們不僅可以追加單個(gè)元素膳算,還能夠追加另外一個(gè)切片座硕。
slice1 := []int{0, 1, 2, 3, 4}
slice2 := []int{55, 66, 77}
fmt.Println(slice1)
slice1 = Append(slice1, slice2...)
fmt.Println(slice1)
// [0 1 2 3 4]
// [0 1 2 3 4 55 66 77]
當(dāng)然,可以在不調(diào)用Extend
的情況下涕蜂,實(shí)現(xiàn)Append
华匾。
func Append(slice []int, elements ...int) []int {
n := len(slice)
total := len(slice) + len(elements)
if total > cap(slice) {
newSize := total*3/2 + 1
newSlice := make([]int, total, newSize)
copy(newSlice, slice)
slice = newSlice
}
slice = slice[:total]
copy(slice[n:], elements)
return slice
}
注意新版的Append
調(diào)用了兩次copy
,第一次將舊切片的內(nèi)容復(fù)制到新切片宇葱,第二次將要加入的元素復(fù)制到新切片瘦真。
slice1 := []int{0, 1, 2, 3, 4}
slice2 := []int{55, 66, 77}
fmt.Println(slice1)
slice1 = Append(slice1, slice2...)
fmt.Println(slice1)
// [0 1 2 3 4]
// [0 1 2 3 4 55 66 77]
內(nèi)置的Append函數(shù)
現(xiàn)在終于談到內(nèi)置的Append
函數(shù)了。內(nèi)置的Append
和上面的示例實(shí)現(xiàn)一樣的功能黍瞧,并且對(duì)任何類型都適用。
要記住的是原杂,在調(diào)用Append
后切片的header會(huì)改變印颤,所以需要保存返回的切片。事實(shí)上穿肄,如果調(diào)用了Append
而沒(méi)有保存結(jié)果的話年局,編譯的時(shí)候就會(huì)報(bào)錯(cuò)际看。
下面是一些使用的例子
slice := []int{1, 2, 3}
slice2 := []int{55, 66, 77}
fmt.Println("Start slice: ", slice)
fmt.Println("Start slice2: ", slice2)
// Start slice: [1, 2, 3]
// Start slice2: [55, 66, 77]
slice = append(slice, 4)
fmt.Println("Add one item:", slice)
// Add one item: [1, 2, 3, 4]
slice = append(slice, slice2...)
fmt.Println("Add one slice:", slice)
// Add one slice: [1, 2, 3, 4, 55, 66, 77]
slice3 := append([]int(nil), slice...)
fmt.Println("Copy a slice:", slice3)
// Copy a slice: [1, 2, 3, 4, 55, 66, 77]
fmt.Println("Before append to self:", slice)
slice = append(slice, slice...)
fmt.Println("After append to self:", slice)
// After append to self: [1 2 3 4 55 66 77 1 2 3 4 55 66 77]
Nil
現(xiàn)在來(lái)看值為nil
的切片。很自然的矢否,nil
是slice header的零值
sliceHeader{
Length: 0,
Capacity: 0,
ZerothElement: nil,
}
或者
sliceHeader{}
元素指針也是nil
仲闽。
由數(shù)組array[0:0]
創(chuàng)建的切片,長(zhǎng)度為0(甚至容量也是0)僵朗,但是元素指針不是nil
赖欣,因此它不是nil
切片,容量可以擴(kuò)大验庙。而值為nil
的切片容量不可能擴(kuò)大顶吮,因?yàn)樗鼪](méi)有指向任何數(shù)組元素。
這也就是說(shuō)粪薛,nil
切片在功能上等同于零長(zhǎng)度的切片悴了,即使它沒(méi)有指向任何數(shù)組。它的長(zhǎng)度為0违寿,可以被通過(guò)重新分配來(lái)擴(kuò)展湃交。
字符串
在了解了切片的基礎(chǔ)上,我們來(lái)聊聊字符串藤巢。
字符串實(shí)際上非常簡(jiǎn)單:它是只讀的切片搞莺,類型為byte
,并且有著語(yǔ)法層面上的一些特性菌瘪。
因?yàn)樽址侵蛔x的腮敌,不能被修改,所以沒(méi)必要考慮容量俏扩。
可以通過(guò)索引的方式訪問(wèn)其中的元素
slash := "/usr/ken"[0]
// /
可以通過(guò)切片來(lái)獲取子串
usr := "/usr/ken"[0:4]
// /user
可以通過(guò)byte切片來(lái)創(chuàng)建一個(gè)字符串
str := string(slice)
或者通過(guò)字符串來(lái)創(chuàng)建一個(gè)切片
slice += []byte(usr)
對(duì)使用者來(lái)說(shuō)糜工,字符串對(duì)應(yīng)的數(shù)組是不可見的,只能操作字符串來(lái)訪問(wèn)其中的元素录淡。這意味著捌木,由字符串轉(zhuǎn)切片或者由切片轉(zhuǎn)字符串,必須創(chuàng)建一份數(shù)組的拷貝嫉戚。當(dāng)然刨裆,Go語(yǔ)言已經(jīng)處理好了這一切,使用者不用再操心彬檀。在轉(zhuǎn)換完成后帆啃,修改切片指向的數(shù)組不會(huì)影響到原始的字符串。
使用類似切片的方式來(lái)構(gòu)建字符串又一個(gè)很明顯的好處窍帝,就是創(chuàng)建子字符串的操作非常高效努潘。并且由于字符串是只讀的,字符串和子串可以安全地共享共同的數(shù)組。
結(jié)語(yǔ)
理解切片的實(shí)現(xiàn)方式疯坤,對(duì)理解切片是如何工作是非常有幫助的报慕。當(dāng)理解了切片的工作方式,切片可以在使用者手里變的簡(jiǎn)單又高效压怠。
“本譯文僅供個(gè)人研習(xí)眠冈、欣賞語(yǔ)言之用,謝絕任何轉(zhuǎn)載及用于任何商業(yè)用途菌瘫。本譯文所涉法律后果均由本人承擔(dān)蜗顽。本人同意簡(jiǎn)書平臺(tái)在接獲有關(guān)著作權(quán)人的通知后,刪除文章突梦〗刖耍”