1. 程式人生 > 其它 >go find 陣列_Go語言 array/slice的用法和內部機制

go find 陣列_Go語言 array/slice的用法和內部機制

技術標籤:go find 陣列

簡介

Go語言的slice(切片,後面統一使用slice)型別為處理一組同類型資料提供了便捷的方法。它與其它程式語言的“陣列”有些類似,但相對而言包含了更多的特性。通過這篇文章,我們來看一看slice到底是什麼,如何去使用它。

Arrays(陣列)

Slice是建立在arrary(陣列)型別上的一種抽象,要理解slice,我們必須先了解一下陣列。

array的定義中包含一個數組長度和元素型別欄位。舉個例子,型別[4]int 表示一個有四個整型數的陣列。陣列的型別是固定的,它的長度也是型別的一部分,因此 [4]int 和 [5]int 是兩個完全不同的資料型別。我們可以使用下標對陣列進行檢索,表示式s[n] 即陣列的第n個元素(從0開始)。

var a [4]inta[0] = 1i := a[0]// i == 1

使用者不需要對陣列進行顯式初始化,一個零值的陣列的所有元素預設被初始化為0:

// a[2] == 0, the zero value of the int type

型別 [4]int 的一個數組在記憶體中表現為四個連續存放的整型數:

bbe031e6a76a9ef560ab315052ee38f0.png

Go語言中,陣列是“值”,一個數組變數代表整個陣列;注意,與C語言不同,它不是指向陣列首元素的指標。這意味著,當你把一個數組變數進行傳遞或賦值時,你會得到它的一份拷貝。為了避免拷貝,可以傳遞指向該陣列的指標。你可以把陣列當成一種結構體(struct),只是通過下標而不是欄位名獲取元素,或者當成一個固定大小的組合值。

我們可以使用下面這種方式定義一個數組:

b := [2]string{"Penn", "Teller"}

不指定元素個數也可以,編譯器會自動計算:

b := [...]string{"Penn", "Teller"}

在上面兩個例子中,b 的型別都是[2]string。

Slices(切片)

陣列有一些應用場景,但是不太靈活,所以在go語言的程式碼中不經常出現。但是切片可以隨處可見。切片建立在陣列之上,但是功能和易用上都更勝一籌。

切片的型別規格是 []T,這裡 T 是元素的型別。不像陣列,切片沒有特定的長度。

切片變數的定義和陣列有些類似,但是不用定義長度:

letters := []string{"a", "b", "c","d"}

切片也可以使用make函式進行建立,make的語言規格如下:

func make([]T, len, cap) []T

這裡 T 表示將被建立切片的元素型別。Make函式接受三個引數:型別長度(length)容量(capacity)。第三個引數是可選的,如果不設定,則與“長度”一致。被呼叫時,make分配一個數組,然後返回一個指向該陣列的slice。

var s []bytes = make([]byte, 5, 5)// s == []byte{0, 0, 0, 0, 0}

下面這行程式碼實現了同樣的效果:

s := make([]byte, 5)

我們可以使用len和cap函式分別檢視切片的長度和容量:

len(s) == 5cap(s) == 5

下兩個環節我們會討論長度和容量的關係。

切片的零值是 nil。使用len和cap函式時,返回值都是0。

還有一種建立切片的方式:slicing(切割)。切割操作時通過一個半開的域來定義的,語法上表現為使用冒號分開的兩個下標。舉個例子,表示式 b[1:4] 會建立一個包含b第1、2、3位置三個元素的切片,新切片的長度是3:

b := []byte{'g', 'o', 'l', 'a', 'n', 'g'}// b[1:4] == []byte{'o', 'l', 'a'}, sharing the same storage as b

起始和結束的下標都是可選的,預設值分別是0和原始切片的長度:

// b[:2] == []byte{'g', 'o'}// b[2:] == []byte{'l', 'a', 'n', 'g'}// b[:] == b

基於陣列建立切片的語法類似:

x := [3]string{"Лайка", "Белка", "Стрелка"}s := x[:] // a slice referencing the storage of x

slice的內部機制

切片是陣列片段的描述符,它包含一個指向陣列的指標、片段的長度、容量(片段的最大長度)。

50466d451ab73c8274890e0351939ac3.png

之前通過make([]byte,5)建立的變數s的記憶體結構如下:

b60eb408f6850d7beb4dff12349e88e2.png

長度(length)即切片中元素的個數

容量(capacity)是slice基於的陣列的元素個數(從slice指標指向的第一個元素開始計算)。

後面我們還會講幾個例子,長度和容量的差別會越來越清晰。

我們對 s 進行切割,觀察資料結構的變化,以及與底層陣列關係的變化。

s = s[2:4]
0bc58279db632ebe9c9372f9b19171ef.png

切割並不會拷貝原切片的資料,而是建立一個新的切片,新切片指向原切片底層的陣列。所以切割操作的效率非常高,因此帶來的一個副作用是修改新切片元素的值時,也會修改老切片的值:

d := []byte{'r', 'o', 'a', 'd'}e := d[2:] // e == []byte{'a', 'd'}e[1] = 'm'// e == []byte{'a', 'm'}// d == []byte{'r', 'o', 'a', 'm'}

之前,我們對 s 進行切割後,s 的長度已經小於容量(見上圖)。通過切割我們可以把 s的長度調整成和容量一致。

s = s[:cap(s)]
573ec45d22f9462f645215c70d1e5909.png

增長切片(通過copy和append函式)

如果要增加一個切片變數的長度,你必須建立一個新的、更大切片變數,然後將原切片的內容拷貝過去。這項技術時從其它語言的動態陣列學來的。下一個例子中,我們將通過建立一個新切片t 來將原切片 s 的容量擴大一倍,然後將 s 的內容拷貝到 t,最後將 t 賦值給 s。

// +1 以免 cap(s) == 0t := make([]byte, len(s), (cap(s)+1)*2) for i := range s {  t[i] = s[i]                }s = t

遍歷賦值的操作可以使用內建的copy函式實現。這個函式正如其名,將資料從一個切片拷貝到另一個切片,返回拷貝元素的數量。

func copy(dst, src []T) int

copy 函式支援在不同長度的切片之間拷貝資料(以元素個數較少的為準)。另外,如果兩個切片共享一個底層陣列,即便兩個切片的資料存在重疊部分,copy 函式也能正確處理。

使用 copy 函式,上面的程式碼可以簡化為:

t := make([]byte, len(s), (cap(s)+1)*2)copy(t, s)s = t

切片的一個常用操作是向末尾新增資料。AppendByte 函式支援向byte切片新增byte元素,必要時自動增長切片,返回更新過的切片。

func AppendByte(slice []byte, data ...byte) []byte {    m := len(slice)        n := m + len(data)    if n > cap(slice) { // if necessary, reallocate        // allocate double what's needed, for future growth.        newSlice := make([]byte, (n+1)*2)        copy(newSlice, slice)        slice = newSlice    }    slice = slice[0:n]    copy(slice[m:n], data)        return slice}

你可以像下面這樣使用AppendByte:

p := []byte{2, 3, 5}p = AppendByte(p, 7, 11, 13)// p == []byte{2, 3, 5, 7, 11, 13}

類似於AppendByte的函式非常實用,即便切片在不斷增長,也能夠完全應付得來。考慮到不同程式的具體情況,剛開始可能需要分配一個較小或較大的記憶體,或限制重新分配的大小。

但是大多數程式並不需要完全掌控這些細節,因此Go語言提供了內建的 append函式,該函式能夠應付大多數情況下的需求。該函式的語言規格是:

func append(s []T, x ...T) []T

append 函式可以將元素x新增到切片 s,並在需要更大的容量時,增長切片。

a := make([]int, 1)// a == []int{0}a = append(a, 1, 2, 3)// a == []int{0, 1, 2, 3}

如果要把一個切片追加到另一個切片的末尾,使用 ... 擴充套件引數列表:

a := []string{"John", "Paul"}b := []string{"George", "Ringo", "Pete"}a = append(a, b...) // 等價於 "append(a, b[0], b[1], b[2])"// a == []string{"John", "Paul", "George", "Ringo", "Pete"}

由於切片的零值(nil)和零長度表現是一致的,你可以宣告一個切片,然後在一個迴圈裡對它賦值:

// Filter returns a new slice holding only// the elements of s that satisfy f()func Filter(s []int, fn func(int) bool) []int {    var p []int // == nil    for _, v := range s {        if fn(v) {            p = append(p, v)        }    }    return p}

一個可能的“坑”

之前提到,重新切割不會拷貝底層的陣列,所以整個陣列會一致保留在記憶體中,知道沒有變數去引用它。在極少數情況下,這可能會導致程式把一大整塊資料都保留在記憶體中,而只用到極少的一部分。

舉個例子,FindDigits函式載入一個檔案到記憶體中,查詢一組連續的數字,並作為一個新的slice返回。

var digitRegexp = regexp.MustCompile("[0-9]+") func FindDigits(filename string) []byte {    b, _ := ioutil.ReadFile(filename)    return digitRegexp.Find(b)}

這段程式碼表現很正常,但是返回了的 []byte 指向的陣列包含整個檔案的內容。由於切片指向原始陣列,只要這個切片存在,gc就不會釋放底層陣列。極少有用的資料卻把整個檔案的內容卡在記憶體裡。

為了修正這個問題,你可以把有用的資料存放到一個新的切片中,然後返回它:

func CopyDigits(filename string) []byte {    b, _ := ioutil.ReadFile(filename)    b = digitRegexp.Find(b)    c := make([]byte, len(b))    copy(c, b)    return c}

這個函式更準確的版本可以藉助於append實現,這裡留給讀者去思考。

擴充套件閱讀

Effective Go包含slice和arrary的深入探討,Go語言規格定義了slice和相關的輔助函式。

原作者:Andrew Gerrand,翻譯:趙帥虎

相關連結:

原文連結:https://blog.golang.org/go-slices-usage-and-internals

Effective Go:http://golang.org/doc/effective_go.html

Effective Go slices:http://golang.org/doc/effective_go.html#slices

Go 語言規格:http://golang.org/doc/go_spec.html

Go 語言規格 slices: http://golang.org/doc/go_spec.html#Slice_types