Go解密:數組、切片

最近在翻閱Go部分源代碼,略有涉及到數組(array)和切片(slice)的實現,本文出自Arrays, slices (and strings): The mechanics of 'append'(https://blog.golang.org/slices) 的中文翻譯版本,我並無徹底按照原文翻譯,部份內容我從新作了解釋以及加入我的理解,還有部份內容我作了適當的刪除和簡化,若是不當之處多多指正。 golang

·介紹
·數組
·Slice:Slice Header
·函數傳遞Slice
·Slice 指針:方法接收器
·make
·copy
·append:示例編程

介紹

編程語言中最多見的一個概念是數組,看起來是彷佛很簡單,但在將數組添加到編程語言時必須考慮許多問題,例如:數組

  1. 固定大小或可變大小?
  2. 大小是不是類型的一部分?
  3. 多維數組的模型?
  4. 空數組的意義?

這些問題的答案會影響數組是編程語言的衆多特性之一仍是其核心設計。 數據結構

在Go早期的開發階段,設計數組前大約花了一年的時間來解決這些問題,關鍵之一是引入Slice:在一個固定大小的數組上有一個靈活且可擴展的數據結構。app

數組

數組是Go中重要模塊,像其餘基礎模塊同樣數組隱藏在一些可見組件之下。在談及功能更增強大、突出的切片以前先簡單說說數組。 編程語言

在Go程序中常常看不到數組,由於數組的大小是數組類型的組成部分,這點限制了書面表達能力。函數

var buffer [256]byte

以上定義了數組變量buffer,類型是[256]byte,類型中描述了大小是256,從這裏能夠理解:[256]byte[512]byte是不一樣的數組類型。 翻譯

與數組有關的數據是元素,數組在內存中的樣子以下:設計

buffer:byte byte byte ...... byte byte byte

這個變量擁有256字節的數據,除此別無其它。能夠經過下標訪問其元素:buffer[0],buffer[1]等,若是索引值超過256訪問數組元素會引發panic。指針

Slice:Slice Header

恐怕這裏有個疑問:Slice的應用場景是什麼?只有理解Slice是什麼和Slice能作什麼才能準確使用。

Slice被描述爲:與Slice自己分開存儲的數組的連續部分的數據結構,Slice不是數組,它描述一個數組。

能夠用以下方式定義Slice變量:

var slice []byte = buffer[100:150]
var slice = buffer[100:150]
slice := buffer[100:150]

因此Slice到底是什麼?在Go源代碼目錄下reflectvalue.go這個文件中找到sliceHeader的定義:

type sliceHeader struct {
    Data unsafe.Pointer
    Len  int
    Cap  int
}

因而咱們能夠暫且對Slice作以下的理解(僞代碼,暫時忽略Cap變量):

slice := sliceHeader{
    Len: 50,
    Data: &buffer[100],
}

正如上,在數組上構建了一個Slice,一樣能夠在Slice上構建Slice:

slice2 := slice[5:10]

根據咱們對Slice的理解,slice2的範圍是[5, 10),放到原始數組上即[105, 110),那麼slice2的結構應該是這樣子:

slice2 := sliceHeader{
    Len: 5,
    Data: &buffer[105],
}

以上能夠知道:slice和slice2仍然指向同一個底層buffer數組。

如今嘗試從新構建slice:從新截取一個Slice,並把新的Slice做爲結果返回給原始Slice結構。

slice = slice[5:10]

這種狀況下,slice看起來和slice2同樣,再截取一次:

slice = slice[1:len(slice) - 1]

對應的sliceHeader:

slice = sliceHeader{
    Len: 8,
    Data: &buffer[101]
}

能夠聯想到Slice的應用場景之一:截取。

函數傳遞Slice

理解Slice包含原始數組指針同時它又是一個值這點很重要,Slice是一個包含了指針和長度的struct。

考慮以下的代碼:

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

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的傳遞規則是值傳遞,值傳遞過程當中拷貝的是sliceHeader結構,並未改變內部指針,該Slice和原Slice都指向同一個數組,當函數返回時候,原數組元素已被修改。

func SubtractOneFromLength(slice []byte) []byte {
    slice = slice[0 : len(slice)-1]
    return slice
}

func main() {
    slice := buffer[10:20]
    for i := 0; i < len(slice); i++ {
        slice[i] = byte(i)
    }

    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是值傳遞,由於進入SubtractOneFromLength只是slice的一個拷貝值,因此先後slice的長度都不變。若是某個函數想修改Slice長度,一個可行的方法是把新的Slice做爲結果參數返回。

Slice 指針:方法接收器

另外一個修改Slice的方法是以指針方式傳遞,上一節的代碼能夠改爲這種:

func PtrSubtractOneFromLength(slicePtr *[]byte) {
    slice := *slicePtr
    *slicePtr = slice[0 : len(slice)-1]
}

func main() {
    slice := buffer[10:20]
    for i := 0; i < len(slice); i++ {
        slice[i] = byte(i)
    }

    fmt.Println("Before: len(slice) =", len(slice))
    PtrSubtractOneFromLength(&slice)
    fmt.Println("After:  len(slice) =", len(slice))
}

這種方法有點累贅,多了一個臨時變量作中轉,對於要修改Slice的函數來講,使用指針傳遞也是比較常見的方式。還有一種方式:

type path []byte

func (p *path) TruncateAtFinalSlash() {
    i := bytes.LastIndex(*p, []byte("/"))
    if i >= 0 {
        *p = (*p)[0:i]
    }
}

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.TruncateAtFinalSlash()
    fmt.Printf("%s\n", pathName)

    pathName1 := path("/usr/bin/tso")
    pathName1.ToUpper()
    fmt.Printf("%s\n", pathName1)
}

// output:
/usr/bin
/USR/BIN/TSO

若是咱們將TruncateAtFinalSlash改成value receiver會發現並無改變原數組,而ToUpper不管是value receiver仍是point receiver都會改變原數組。這也是Slice有趣的一點,Slice在函數傳遞中是值傳遞(拷貝變量值,內部指針仍舊指向原數組),若TruncateAtFinalSlash爲value receiver,在進行p = (p)[0:i]操做時,p將會是一個新的Slice而不是pathName,而在ToUpper中,以Slice方式操做底層原數組,不管是哪一種receiver都將改變原數組。

容量

正如前面所說,sliceHeader中還有一個Cap變量,這個變量存儲了Slice的容量,記錄數組實際使用了多少的空間,這是Len能達到的最大值。看看這樣的代碼:

func main() {
    var array [10]int
    for i := 0; i < 10; i++ {
        array[i] = i
    }

    slice := array[6:10]
    fmt.Printf("%v, %v, %v, %p\n", slice, cap(slice), len(slice), &slice[0])

    slice = append(slice, 11)
    fmt.Printf("%v, %v, %v, %p\n", slice, cap(slice), len(slice), &slice[0])

    slice[0] = 12
    fmt.Printf("%v, %v, %v, %p\n", slice, cap(slice), len(slice), &slice[0])

    fmt.Println(array)
}

// out put
[6 7 8 9], 4, 4, 0xc00006a0d0
[6 7 8 9 11], 8, 5, 0xc00007c100
[12 7 8 9 11], 8, 5, 0xc00007c100
[0 1 2 3 4 5 6 7 8 9]

因而咱們知道,當向Slice追加元素致使Cap大於Len時會建立一個Cap大於原數組的新數組(首元素地址不一致),並將值拷貝進新數組,以後再改變Slice元素值時改變的是新建立的數組(切斷與原數組的引用關係)。

make

根據Slice的定義:Cap限制了Slice的增加,當想增大Slice到大於自己容量時,推薦的作法是建立新的數組,而後把Slice數據拷貝到新數組。使用make建立一個新的數據並建立一個Slice。make有三個參數:Slice類型,初始長度和容量,用於存儲Slice數據的數組長度,默認狀況下,

func main() {
    slice := make([]int, 10, 15)
    fmt.Printf("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.Printf("len: %d, cap: %d\n", len(slice), cap(slice))
}

// output
len: 10, cap: 15
len: 10, cap: 30

make建立了一個新的Slice,而後將原數據拷貝至新的Slice(所指向的數組)。

copy

Go有個內建的copy函數,參數是兩個Slice,將第二個Slice的數據拷貝到第一個Slice中:

func main() {
    slice := make([]int, 10, 15)

    newSlice := make([]int, len(slice), 2*cap(slice))
    copy(newSlice, slice)
}

對於Slice的copy而言,有一點比較繞口:copy複製的元素數量是兩個Slice中長度最小的那個,必定程度上節約效率。

有一種常見的狀況:原Slice和目的Slice出現交叉(在C++中咱們常叫地址重疊),但copy操做任然能正常進行,這意味着copy能夠用於單個Slice移動元素。

// 向Slice指定有效位置插入元素,Slice必須有空間能夠增長新的元素
func Insert(slice []int, index, value int) []int {
    // 增長1個元素的空間
    slice = slice[0:len(slice)+1]
    // 使用copy移動Slice前半部分
    copy(slice[index + 1:], slice[index:])
    // 存放新的值
    slice[index] = value

    return slice
}

Insert函數完成向Slice中插入值,值得注意的是,函數必須返回Slice(前面有獎過爲何)。

append:示例

這裏留一個問題:如何自實現Slice append函數?

相關文章
相關標籤/搜索