數(shù)組和切片:Append的實(shí)現(xiàn)方式

圖文無(wú)關(guān)

本文翻譯自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之間的元素锋华。slice2sliceHeader結(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)人的通知后,刪除文章突梦〗刖耍”

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市宫患,隨后出現(xiàn)的幾起案子刊懈,更是在濱河造成了極大的恐慌,老刑警劉巖娃闲,帶你破解...
    沈念sama閱讀 218,682評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件虚汛,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡皇帮,警方通過(guò)查閱死者的電腦和手機(jī)卷哩,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,277評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)属拾,“玉大人将谊,你說(shuō)我怎么就攤上這事〗グ祝” “怎么了尊浓?”我有些...
    開封第一講書人閱讀 165,083評(píng)論 0 355
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)纯衍。 經(jīng)常有香客問(wèn)我栋齿,道長(zhǎng),這世上最難降的妖魔是什么襟诸? 我笑而不...
    開封第一講書人閱讀 58,763評(píng)論 1 295
  • 正文 為了忘掉前任瓦堵,我火速辦了婚禮,結(jié)果婚禮上歌亲,老公的妹妹穿的比我還像新娘菇用。我一直安慰自己,他們只是感情好陷揪,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,785評(píng)論 6 392
  • 文/花漫 我一把揭開白布刨疼。 她就那樣靜靜地躺著泉唁,像睡著了一般鹅龄。 火紅的嫁衣襯著肌膚如雪揩慕。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,624評(píng)論 1 305
  • 那天扮休,我揣著相機(jī)與錄音迎卤,去河邊找鬼。 笑死玷坠,一個(gè)胖子當(dāng)著我的面吹牛蜗搔,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播八堡,決...
    沈念sama閱讀 40,358評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼樟凄,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了兄渺?” 一聲冷哼從身側(cè)響起缝龄,我...
    開封第一講書人閱讀 39,261評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎挂谍,沒(méi)想到半個(gè)月后叔壤,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,722評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡口叙,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評(píng)論 3 336
  • 正文 我和宋清朗相戀三年炼绘,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片妄田。...
    茶點(diǎn)故事閱讀 40,030評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡俺亮,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出疟呐,到底是詐尸還是另有隱情脚曾,我是刑警寧澤,帶...
    沈念sama閱讀 35,737評(píng)論 5 346
  • 正文 年R本政府宣布萨醒,位于F島的核電站斟珊,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏富纸。R本人自食惡果不足惜囤踩,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,360評(píng)論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望晓褪。 院中可真熱鬧堵漱,春花似錦、人聲如沸涣仿。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,941評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至愉镰,卻和暖如春米罚,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背丈探。 一陣腳步聲響...
    開封第一講書人閱讀 33,057評(píng)論 1 270
  • 我被黑心中介騙來(lái)泰國(guó)打工录择, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人碗降。 一個(gè)月前我還...
    沈念sama閱讀 48,237評(píng)論 3 371
  • 正文 我出身青樓隘竭,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親讼渊。 傳聞我的和親對(duì)象是個(gè)殘疾皇子动看,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,976評(píng)論 2 355

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

  • Slice模型 切片(Slice)可以看作是對(duì)數(shù)組的一種包裝形式.也就是說(shuō),切片的實(shí)現(xiàn)還是數(shù)組.讓我們從創(chuàng)建將起:...
    ywhu閱讀 1,439評(píng)論 0 1
  • 切片(slice)是 Golang 中一種比較特殊的數(shù)據(jù)結(jié)構(gòu),這種數(shù)據(jù)結(jié)構(gòu)更便于使用和管理數(shù)據(jù)集合爪幻。切片是圍繞動(dòng)態(tài)...
    小孩真笨閱讀 1,074評(píng)論 0 1
  • 線性結(jié)構(gòu)是計(jì)算機(jī)最常用的數(shù)據(jù)結(jié)構(gòu)之一菱皆。無(wú)論是數(shù)組(arrary)還是鏈表(list),在編程中不可或缺笔咽。golan...
    _二少爺閱讀 6,611評(píng)論 5 13
  • 切片(slice)是 Golang 中一種比較特殊的數(shù)據(jù)結(jié)構(gòu)搔预,這種數(shù)據(jù)結(jié)構(gòu)更便于使用和管理數(shù)據(jù)集合。切片是圍繞動(dòng)態(tài)...
    51reboot閱讀 28,649評(píng)論 2 10
  • 一大早被外面的咒罵聲驚醒,看一下表5:58分甩十〈樱可能是昨晚喝了太多的乳酸菌飲料,扁桃體有些發(fā)炎侣监,嗓子疼得直冒火鸭轮。起來(lái)...
    落肥肥閱讀 482評(píng)論 0 0