Go語言之父詳述切片與數組的不一樣

切片是Go 語言核心的數據結構,然而剛接觸 Go 的程序員常常在切片的工做方式和行爲表現上被絆倒。好比,明明說切片是引用類型但在函數內對其作的更改有時候卻保留不下來,有時候卻能夠。究其緣由是由於咱們不少人用其餘語言的思惟來嘗試猜想 Go 語言中切片的行爲,切片這個內置類型在 Go 語言底層有其單獨的類型定義,而不是咱們一般理解的其餘語言中數組的概念。php

文章翻譯自羅伯·派克發佈在 Go Blog 中的文章,文中詳述了切片是如何被設計出來的以及其與數組的關聯和區別,以及內置append函數的實現細節。雖篇幅很長,仍是建議認證讀完,尤爲是關於切片的設計和append函數實現的部分,理解了「切片頭」後不少的切片行爲就天然而然可以理解。程序員

Rob·Pike 2013 年 9 月 26 日 原文地址:blog.golang.org/slicesgolang

介紹

過程編程語言最多見的特徵之一就是數組的概念。數組看似簡單,可是將數組添加到語言時必須回答許多問題,例如:編程

  • 數組使用固定尺寸仍是可變尺寸?
  • 尺寸是數組類型的一部分嗎?
  • 多維數組是什麼樣的?
  • 空數組有意義嗎?

這些問題的答案會影響數組是否只是語言的一個普通的功能仍是其設計的核心部分。數組

在 Go 的早期開發中,在感受到設計正確以前,咱們花了大約一年的時間決定對這些問題的答案。很是關鍵的一步是咱們引入了切片,它基於固定大小的數組構建,以提供靈活,可擴展的數據結構。然而,直到今天,剛接觸 Go 的程序員常常在切片的工做方式上被絆倒,這也許是由於其餘語言的經驗固化了他們的思惟。安全

在這篇文章中,咱們將嘗試消除混亂。咱們將經過構建知識片斷來解釋 append 內置函數的工做原理以及它如此工做的緣由。bash

數組

數組是 Go 中重要的構建塊,但就像建築物的基礎同樣,它們一般隱藏在可見的組件下。在繼續介紹切片的更有趣,更強大和更重要的概念以前,咱們必須簡短地談論一下數組。數據結構

在 Go 程序中並不常常看到數組,由於數組的大小是數組類型的一部分,這限制了數組的表達能力。app

聲明數組以下編程語言

var buffer [256]byte
複製代碼

聲明數組變量 buffer,其中包含 256 個字節。 buffer 的類型包括其大小,[256] byte。 一個包含 512 個字節的數組將具備不一樣的類型 [512] byte

與數組關聯的數據就是:元素數組。從原理上講,咱們的 buffer 在內存中看起來像這樣,

buffer: byte byte byte ... 256 個 ... byte byte byte
複製代碼

也就是說,該變量保存 256 個字節的數據,僅此而已。咱們能夠經過使用熟悉的索引語法 buffer [0]buffer [1]buffer [255] 等訪問其元素。 (索引範圍 0 到 255 涵蓋 256 個元素。) 嘗試使用該範圍以外的值索引數組 buffer 會使程序崩潰。

內置函數 len 的回數組或切片以及其餘一些數據類型的元素數量。對於數組,很明顯 len 會返回什麼。在咱們的示例中,len(buffer) 返回固定值 256。

數組有本身的一席之地 (例如,它們很好地表示了轉換矩陣),可是它們在 Go 中最多見的應用目的是保留切片的存儲空間。

Slices:切片頭

切片是執行操做的地方,可是要充分利用它們,開發者必須準確瞭解它們的含義和做用。

切片是一種數據結構,描述與切片變量自己分開存儲的數組的一段連續的部分,。 切片不是數組。切片描述一塊數組。

用上節給定的數組變量 buffer,咱們能夠建立一個描述了數組第 100 個元素到第 150 個元素的切片(準確地說是包含第 100 個元素到 149 個元素):

var slice []byte = buffer[100:150]
複製代碼

在該代碼段中,咱們使用了完整的變量聲明。變量slice 的類型爲 [] byte 的 「字節切片」,並經過從名爲 buffer 的數組切片第 100 個元素 (包括) 到第 150 個元素 (不包括) 來初始化。更慣用的語法是忽略類型,類型由初始化表達式設置:

var slice = buffer[100:150]
複製代碼

在函數內部,咱們可使用簡短聲明形式,

slice := buffer[100:150]
複製代碼

切片變量究竟是什麼?如今將 slice 看做是一個具備兩個元素的小數據結構:長度和指向數組元素的指針。你能夠認爲它是在底層像這樣被構建的:

type sliceHeader struct {
    Length        int
    ZerothElement *byte
}

slice := sliceHeader{
    Length:        50,
    ZerothElement: &buffer[100],
}
複製代碼

固然,這只是一個爲了說明舉的例子。儘管此代碼段說明了 sliceHeader 結構對於程序員是不可見的,而且元素指針的類型取決於元素的類型,但這給出了切片機制大致上的概念。

到目前爲止,咱們已經對數組使用了切片操做,可是咱們也能夠對切片進行切片操做,以下所示:

slice2 := slice[5:10]
複製代碼

和以前同樣,此操做將建立一個新的切片,在這種狀況下,新切片將使用原始切片的元素 5 至 9,也就是原始數組的元素 105 至 109。 slice2 變量底層的 sliceHeader 結構以下所示:

slice2 := sliceHeader{
    Length:        5,
    ZerothElement: &buffer[105],
}
複製代碼

請注意,此標頭仍指向存儲在 buffer 變量中的相同底層數組。

咱們還能夠重切片,也就是說對切片進行切片操做,而後將結果存儲回原始切片結構中。在執行下面的切片操做後

slice = slice[5:10]
複製代碼

slice 變量的 sliceHeader 結構看起來和 slice2 變量的結構同樣。在使用Go 的過程當中你將會看到重切片會被常用,例如截斷切片。下面的語句刪除切片的第一個和最後一個元素:

slice = slice[1:len(slice)-1]
複製代碼

[練習:在上面的賦值以後,寫出 sliceHeader 結構的外觀。]

你將常常會聽到經驗豐富的 Go 程序員談論 「切片標頭」,由於這其實是存儲在切片變量中的內容。例如,當您調用一個將切片做爲參數的函數時,例如bytes.IndexRune,該標頭就是傳遞給該函數的內容。在此次調用中

slashPos := bytes.IndexRune(slice, '/')
複製代碼

傳遞給 IndexRune 函數的 slice 參數其實是一個 「切片標頭」。

切片頭中還有一個數據項,咱們將在下面討論,可是首先讓咱們看看在使用切片進行編程時,切片 頭的存在乎味着什麼。

將切片傳遞給函數 重要的是要理解,即便切片包含指針,它自己也是一個值。在幕後,它是一個結構體值,包含一個指針和一個長度。它不是結構體的指針。

這很重要。

在上一個示例中,當咱們調用IndexRune 時,它傳遞了切片頭的副本。這種行爲具備重要的影響。

考慮下面這個簡單的函數

func AddOneToEachElement(slice []byte) {
    for i := range slice {
        slice[i]++
    }
}
複製代碼

它確實作到了其名稱暗示的那樣,對切片的索引進行迭代 (使用 for range 循環),自增每一個元素。

嘗試一下:

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)
}
複製代碼

(若是想探究,能夠編輯並從新執行這些可運行的代碼段。)

儘管切片頭是按值傳遞的,但標頭包含指向數組元素的指針,所以原始切片標頭和傳遞給函數的標頭副本都描述了同一數組。因此,當函數返回時,能夠經過原始slice變量看到修改後的元素。

該函數的參數其實是一個切片的標頭副本,如如下示例所示:

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))
}
複製代碼

在這裏咱們看到slice參數的內容能夠由函數修改,可是它的切片標頭不能。調用該函數不會修改slice 變量中存儲的長度,由於傳給該函數的是切片頭的副本 (而不是原始頭)。所以,若是咱們要編寫一個修改標頭的函數,則必須像在此所作的同樣,將其做爲結果參數返回。 slice 變量不變,但返回的值具備新長度,而後將其存儲在 newSlice 中,

指向切片的指針:方法接收者

另外一種讓函數修改切片頭的方法是將指向切片的指針傳遞給函數,下面是咱們以前的示例的一個變體:

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))
}
複製代碼

這個例子看起來很笨拙,尤爲是還須要處理額外的間接尋址(使用臨時變量實現),可是有一種狀況咱們會常常看到指向切片的指針:一個會修改切片的方法的慣用模式是使用切片的指針做爲方法的接收者。

假設咱們想在切片上有一個方法,以便在最後一個斜槓處將其截斷。咱們能夠這樣寫:

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") // 將字符串轉換爲 path 類型
    pathName.TruncateAtFinalSlash()
    fmt.Printf("%s.", pathName)
}
複製代碼

若是運行此示例,您將看到它能夠正常工做,並在調用的函數中更新切片。

[練習:將接收器的類型更改成值而不是指針,而後再次運行。解釋發生了什麼。]

另外一方面,若是咱們想爲path類型編寫一個方法,該方法會將路徑中的ASCII字母轉爲大寫,則該方法的接口者能夠是一個切片值,由於值接收者仍然會指向相同的基礎數組。

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.Printf("%s.", pathName)
}
複製代碼

在這裏,ToUpper 方法在中爲range 循環使用兩個變量來捕獲索引和切片元素。這種形式的循環避免在體內屢次寫入p[i]

[練習:轉換 ToUpper 方法以使用指針接收器,並查看其行爲是否改變。]

[高級練習:轉換 ToUpper 方法以處理 Unicode 字母,而不只僅是ASCII。]

容量

下面這個函數爲其整型切片參數擴充一個元素:

func Extend(slice []int, element int) []int {
    n := len(slice)
    slice = slice[0 : n+1]
    slice[n] = element
    return slice
}
複製代碼

(爲何它須要返回修改後的切片?) 如今使用 Extend 函數並運行下面的程序:

func main() {
    var iBuffer [10]int
    slice := iBuffer[0:0]
    for i := 0; i < 20; i++ {
        slice = Extend(slice, i)
        fmt.Println(slice)
    }
}
複製代碼

看看切片如何增加,直到... 它不會增加。

如今該討論切片標頭的第三個組成部分:容量。除了數組指針和長度,切片頭還存儲其切片容量:

type sliceHeader struct {
    Length        int
    Capacity      int
    ZerothElement *byte
}
複製代碼

Capacity字段記錄基礎數組實際有多少空間;它是Length 能夠達到的最大值。試圖使切片超出其容量將超出切片的底層數組的限制,這會引起 panic

在咱們的示例切片經過下面的語句建立以後,

slice := iBuffer[0:0]
複製代碼

它的切片頭會是這樣:

slice := sliceHeader{
    Length:        0,
    Capacity:      10,
    ZerothElement: &iBuffer[0],
}
複製代碼

Capacity 字段等於基礎數組的長度減去切片的第一個元素指向的數組元素在數組中的索引 (在本例中切片第一個元素對應的數組元素的索引爲 0)。若是要查詢切片的容量,請使用內置函數 cap

if cap(slice) == len(slice) {
    fmt.Println("slice is full!")
}
複製代碼

Make 函數

若是咱們想將切片擴大到超出其capacity怎麼辦?實際上你辦不到!根據定義,capacity 是切片增加的極限。可是,您能夠經過分配一個新數組,複製數據到新數組並修改切片以描述新的數組來得到等效的結果。

讓咱們從分配開始。咱們可使用 new 內置函數分配一個更大的數組,而後對結果進行切片,可是使用 make 內置函數更簡單。它分配一個新數組並建立一個切片頭來描述它。 make函數採用三個參數:切片的類型,初始長度和容量,容量是 make分配的用來保存切片數據的數組的長度。下面make函數的調用,能夠建立一個長度爲 10 的切片,底層數組還有 5 個餘量 (15-10),這能夠經過運行它看到:

slice := make([]int, 10, 15)
fmt.Printf("len: %d, cap: %d.", len(slice), cap(slice))
複製代碼

下面的代碼片斷使咱們的 int 切片的容量增長了一倍,但長度保持不變:

slice := make([]int, 10, 15)
fmt.Printf("len: %d, cap: %d.", len(slice), cap(slice))
newSlice := make([]int, len(slice), 2*cap(slice))
for i := range slice {
    newSlice[i] = slice[i]
}
slice = newSlice
fmt.Printf("len: %d, cap: %d.", len(slice), cap(slice))
複製代碼

運行上面的代碼後,slice 在須要再次分配新的底層數組以前擁有了更多空間去擴充。

建立切片時,長度和容量一般是相同的。內置的make支持此常見狀況的簡寫形式。 length 參數值默認爲capacity值,所以在使用make函數時您能夠省略capacity將它們設置爲相同的值。像下面這樣:

gophers := make([]Gopher, 10)
複製代碼

gophers切片的長度和容量都被設置爲 10。

Copy 函數

在上一節中將切片的容量加倍時,咱們編寫了一個循環,將舊數據複製到新切片。 Go 具備內置函數 copy,可簡化此操做。它的參數是兩個切片,它將數據從右側參數複製到左側參數。下面咱們使用 copy 函數重寫上節的示例:

newSlice := make([]int, len(slice), 2*cap(slice))
copy(newSlice, slice)
複製代碼

copy 函數很智能。它只複製它能夠複製的內容,會關注兩個參數的長度。換句話說,它複製的元素數量是兩個切片長度中的最小值。這樣能夠節省一些記錄操做。一樣,copy 返回一個整數值,即它複製的元素數量,儘管這個返回值並不老是值得在程序中檢查。

當源切片和目標切片重疊時,copy 函數也能夠正確處理,這意味着它能夠用於在單個切片中移動元素。如下是使用 copy 將值插入切片中間的方法。

//Insert 函數將值插入到切片指定的索引位置上
//插入的位置必須在範圍內。
//切片必須爲新元素留出空間。
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
}
複製代碼

在這個函數中有兩點須要注意。首先,它必須返回更新的切片,由於其長度已更改。其次,它使用了簡寫的切片表達式

slice[i:]
複製代碼

效果與下面的表達式徹底相同

slice[i:len(slice)]
複製代碼

一樣,儘管咱們尚未使用這個技巧,可是咱們也能夠省略切片表達式的第一個元素,它默認爲零。

slice[:] 上面的表達式表示切片自己,這在切片(動詞)數組時頗有用。下面的表達式是 「描述數組全部元素的切片」 的最快捷的方法:

array[:]
複製代碼

如今,讓咱們運行 Insert 函數。

slice := make([]int, 10, 20) // 注意容量>長度:表明添加元素的空間。
for i := range slice {
    slice[i] = i
}
fmt.Println(slice)
slice = Insert(slice, 5, 99)
fmt.Println(slice)
複製代碼

Append: 一個例子

在前面幾節中,咱們編寫了Extend函數,該函數將切片擴展了一個元素。可是,這個函數是有問題的,由於若是切片的容量過小,該函數將崩潰。 (咱們的 Insert 示例函數也有一樣的問題。) 如今咱們已經解決了這一問題,因此讓咱們爲整數切片編寫一個Extend的可靠實現。

func Extend(slice []int, element int) []int {
    n := len(slice)
    if n == cap(slice) {
        // 切片已滿,必須擴充容量
        // 咱們將其容量加倍並加1,所以若是原來大小爲零,仍能擴展切片容量。
        newSlice := make([]int, len(slice), 2*len(slice)+1)
        copy(newSlice, slice)
        slice = newSlice
    }
    slice = slice[0 : n+1]
    slice[n] = element
    return slice
}
複製代碼

在這個函數中,最後返回切片特別重要,由於當它從新分配時,結果切片描述了一個徹底不一樣的數組。下面的代碼片斷演示了切片填滿時發生的狀況:

slice := make([]int, 0, 5)
for i := 0; i < 10; i++ {
    slice = Extend(slice, i)
    fmt.Printf("len=%d cap=%d slice=%v.", len(slice), cap(slice), slice)
    fmt.Println("address of 0th element:", &slice[0])
}
複製代碼

請注意,當初始大小爲 5 的數組填滿時,發生了數組從新分配。分配新數組時,切片的容量以及第零個元素的地址都會改變。

藉助強大的Extend函數做爲引導,咱們能夠編寫一個更好的函數,使咱們能夠將切片擴展多個元素。爲此,咱們使用Go在調用函數時將函數參數列表轉換爲切片的功能。也就是說,咱們使用 Go的可變函數參數功能。

咱們將新函數命名爲Append。對於第一個版本,咱們能夠重複調用 Extend,這樣可變函數的機制就很清楚了。 Append的函數簽名是這樣的:

func Append(slice []int, items ...int) []int
複製代碼

Append接受一個切片參數,而後是零個或多個int參數。就Append的實現而言,這些參數正是一個int 型切片,如您所見:

// Append將項目追加到切片
//第一個版本:只是循環調用Extend。
func Append(slice []int, items ...int) []int {
    for _, item := range items {
        slice = Extend(slice, item)
    }
    return slice
}
複製代碼

注意 for range 循環遍歷 items 參數的元素,該參數具備隱式類型[]int。還要注意使用空白標識符_來丟棄循環中的索引,由於在個例子中咱們不須要索引。

嘗試一下:

slice := []int{0, 1, 2, 3, 4}
fmt.Println(slice)
slice = Append(slice, 5, 6, 7, 8)
fmt.Println(slice)
複製代碼

此示例中的另外一項新技術是,咱們經過編寫複合字面量來初始化切片,該複合字面量由切片的類型以及括號中的元素組成:

slice := []int{0, 1, 2, 3, 4}
複製代碼

Append 頗有意思的另外一個緣由是,咱們不只能夠像源切片追加元素,還能夠在調用Append時使用...語法將切片拆分紅函數的實參。這樣咱們就能用Append函數將第二個切片整個追加給源切片了。

slice1 := []int{0, 1, 2, 3, 4}
slice2 := []int{55, 66, 77}
fmt.Println(slice1)
slice1 = Append(slice1, slice2...) //  '...' 是必須的!
fmt.Println(slice1)
複製代碼

固然,咱們能夠在Extend的內部基礎上分配不超過一次的分配來提升Append的效率:

// Append 將元素追加到切片
//高效的版本。
func Append(slice []int, elements ...int) []int {
    n := len(slice)
    total := len(slice) + len(elements)
    if total > cap(slice) {
         //從新分配。增加到新大小的1.5倍,所以咱們仍然能夠增加。
        newSize := total*3/2 + 1
        newSlice := make([]int, total, newSize)
        copy(newSlice, slice)
        slice = newSlice
    }
    slice = slice[:total]
    copy(slice[n:], elements)
    return slice
}
複製代碼

在這裏,請注意咱們如何兩次使用copy 函數的,一次將切片數據移動到新分配的內存中,而後將附加項複製到舊數據的末尾。

嘗試一下;新代碼片斷的行爲與之前相同:

slice1 := []int{0, 1, 2, 3, 4}
slice2 := []int{55, 66, 77}
fmt.Println(slice1)
slice1 = Append(slice1, slice2...) //'...'是必不可少的!
fmt.Println(slice1)
複製代碼

Append: 內置函數

所以,咱們得出了設計append內置函數的動機。它的效率與咱們的Append示例徹底相同,可是它可以適用於任何切片類型。

Go的一個缺點是任何泛型類型的操做都必須由運行時提供。有一天這種狀況可能會改變,可是如今,爲了更容易地處理切片Go提供了一個內置的泛型函數append。它的工做方式與咱們的 int切片版本相同,但適用於任何切片類型

請記住,因爲切片標頭老是經過調用append進行更新,因此須要在調用後保存返回的切片。實際上,編譯器不會讓您在不保存結果的狀況下調用append

下面是一些與print語句混合的線性程序。試試看,編輯並探究結果

// 建立兩個初始切片
slice := []int{1, 2, 3}
slice2 := []int{55, 66, 77}
fmt.Println("Start slice: ", slice)
fmt.Println("Start slice2:", slice2)

//將一個元素添加到切片
slice = append(slice, 4)
fmt.Println("Add one item:", slice)

//將一個切片添加到另外一個切片。
slice = append(slice, slice2...)
fmt.Println("Add one slice:", slice)

//複製(int的)切片。
slice3 := append([]int(nil), slice...)
fmt.Println("Copy a slice:", slice3)

//將切片複製到其自身的末尾。
fmt.Println("Before append to self:", slice)
slice = append(slice, slice...)
fmt.Println("After append to self:", slice)
複製代碼

值得花一點時間仔細考慮該示例的最後一個代碼,以理解切片的設計如何使此簡單調用正確工做成爲可能。

在社區構建的「Slice Tricks」 Wiki 頁面。上,有更多的appendcopy 和其餘使用切片方式的示例。

Nil

順便說一句,有了咱們新學到的知識,咱們能夠看到nil切片的表示是什麼。天然地,它是切片標頭的零值:

sliceHeader{
    Length:        0,
    Capacity:      0,
    ZerothElement: nil,
}
複製代碼

或者這麼表示

sliceHeader{}
複製代碼

關鍵的細節是切片頭中元素指針也是nil,而由下面語句建立的切片

array[0:0]
複製代碼

長度爲零 (甚至容量爲零),但其指針不是nil,所以它不是nil切片。

須要清楚的是,空切片能夠增加 (假設其容量爲非零),可是nil切片沒有數組能夠放入值,甚至不能增加以容納一個元素。

就是說,nil切片在功能上等效於零長度切片,即便它沒有指向任何內容。它的長度爲零,經過分配新數組能夠用append 函數向其追加元素。例如,請查看上面的單線程序,該單線程序經過附加到nil切片來複制切片。

譯註:說的是下面這個程序

//複製(int的)切片。
slice3 := append([]int(nil), slice...)
fmt.Println("Copy a slice:", slice3)
複製代碼

字符串

如今簡要介紹一下切片上下文中的Go中的字符串。

字符串實際上很是簡單:它們只是只讀的字節切片,而切在語言層面還提供了一些額外的語法支持。

由於它們是隻讀的,因此不須要容量 (不能增長它們),可是對於大多數狀況下,您能夠將它們像只讀的字節切片同樣對待他們。

首先,咱們能夠爲它們索引字符串以訪問各個字節:

slash := "/usr/ken"[0] //產生字節值'/'
複製代碼

咱們能夠對字符串進行切片以獲取子字符串:

usr := "/usr/ken"[0:4] // 產生字符串"/usr"
複製代碼

如今,當咱們切成字符串時,幕後發生的事情應該很容易理解了。

咱們還能夠用一個普通的字節切片,經過簡單的轉換從中建立一個字符串:

str := string(slice)
複製代碼

反之亦然:

slice := []byte(usr)
複製代碼

字符串底層的數組從視野中被隱藏掉了;除了經過字符串,沒法訪問其內容。這意味着當咱們執行這些轉換中的任何一個時,都必須複製該數組。固然,Go 會處理好這一點,所以您沒必要這樣作。在這些轉換中的任何一個以後,對字節片下面的數組的修改不會影響相應的字符串。

這種相似切片的字符串設計的一個重要結果是建立子字符串很是高效。全部須要作的就是建立一個兩個字的字符串標頭。因爲字符串是隻讀的,所以原始字符串和切片操做產生的字符串能夠安全地共享同一數組。

歷史記錄:最先的字符串實現老是分配的,可是當將切片添加到語言時,它們提供了有效的字符串處理模型。結果一些基準測試得到了巨大的加速。

固然,字符串還有更多的東西,單獨的博客文章能夠更深刻地瞭解它們。

結論

理解切片的工做原理,有助於瞭解切片的實現方式。切片有一個小的數據結構,即切片標頭,它是與 slice 變量關聯的項目,而且該標頭描述了單獨分配的數組的一部分。當咱們傳遞切片值時,將標頭將會被複制,但始終都會指向它(譯註:源標頭)指向的數組。

一旦瞭解了它們的工做原理,切片不只變得易於使用,並且變得強大而富有表現力,尤爲是在 copyappend內置函數的幫助下。

閱讀更多

Go中有關切片的管間中能夠找到不少東西。如前所述,「Slice Tricks」 Wiki 頁面有不少示例。

有不少可用的資料,可是學習切片的最佳方法是使用切片。

相關文章
相關標籤/搜索