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 的一個數組在記憶體中表現為四個連續存放的整型數:
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的內部機制
切片是陣列片段的描述符,它包含一個指向陣列的指標、片段的長度、容量(片段的最大長度)。
之前通過make([]byte,5)建立的變數s的記憶體結構如下:
長度(length)即切片中元素的個數
容量(capacity)是slice基於的陣列的元素個數(從slice指標指向的第一個元素開始計算)。
後面我們還會講幾個例子,長度和容量的差別會越來越清晰。
我們對 s 進行切割,觀察資料結構的變化,以及與底層陣列關係的變化。
s = s[2:4]
切割並不會拷貝原切片的資料,而是建立一個新的切片,新切片指向原切片底層的陣列。所以切割操作的效率非常高,因此帶來的一個副作用是修改新切片元素的值時,也會修改老切片的值:
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)]
增長切片(通過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