go中的數據結構切片-slice
- 2019 年 11 月 13 日
- 筆記
1.部分基本類型
go中的類型與c的相似,常用類型有一個特例:byte類型,即位元組類型,長度為,默認值是0;
1 bytes = [5]btye{'h', 'e', 'l', 'l', 'o'}
變數bytes的類型是[5]byte,一個由5個位元組組成的數組。它的記憶體表示就是連起來的5個位元組,就像C的數組。
1.1字元串
字元串在Go語言記憶體模型中用一個2字長(64位,32位記憶體布局方式下)的數據結構表示。它包含一個指向字元串數據存儲地方的指針,和一個字元串長度數據如下圖:
s是一個string類型的字元串,因為string類型不可變,對於多字元串共享同一個存儲數據是安全的。切分操作str[i:j]
會得到一個新的2字長結構t,一個可能不同的但仍指向同一個位元組序列(即上文說的存儲數據)的指針和長度數據。所以字元串切分不涉及記憶體分配或複製操作,其效率等同於傳遞下標。
1.2數組
數組類型定義了長度和元素類型。如, [4]int
類型表示一個四個整數的數組,其長度是固定的,長度是數組類型的一部分( [4]int
和 [5]int
是完全不同的類型)。 數組可以以常規的索引方式訪問,表達式 s[n]
訪問數組的第 n 個元素。數組不需要顯式的初始化;數組的零值是可以直接使用的,數組元素會自動初始化為其對應類型的零值。
1 var a [4]int 2 a[0] = 1 3 i := a[0] 4 // i == 1 5 // a[2] == 0, int 類型的零值
Go的數組是值語義。一個數組變數表示整個數組,它不是指向第一個元素的指針(不像 C 語言的數組)。 當一個數組變數被賦值或者被傳遞的時候,實際上會複製整個數組。 (為了避免複製數組,你可以傳遞一個指向數組的指針,但是數組指針並不是數組。) 可以將數組看作一個特殊的struct,結構的欄位名對應數組的索引,同時成員的數目固定。
b := [2]string{"Penn", "Teller"} b := [...]string{"Penn", "Teller"}
這兩種寫法, b
都是對應 [2]string
類型。
2.切片slice
2.1結構
切片類型的寫法是[]T
,T
是切片元素的類型。和數組不同的是,切片類型並沒有給定固定的長度。切片的字面值和數組字面值很像,不過切片沒有指定元素個數:
1 letters := []string{"a", "b", "c", "d"}
2 s := letters [:] //a slice referencing the storage of x 3 func make([]T, len, cap) []T //使用內置函數 make 創建
一個slice是一個數組某個部分的引用。在記憶體中它是一個包含三個域的結構體:指向slice中第一個元素的指針ptr,slice的長度數據len,以及slice的容量cap。長度是下標操作的上界,如x[i]中i必須小於長度。容量是分割操作的上界,如x[i:j]中j不能大於容量。slice在Go的運行時庫中就是一個C語言動態數組的實現,在$GOROOT/src/pkg/runtime/runtime.h中定義:
struct Slice { // must not move anything byte* array; // actual data uintgo len; // number of elements uintgo cap; // allocated number of elements };
數組的slice會創建一份新的數據結構,包含一個指針,一個指針和一個容量數據。如同分割一個字元串,分割數組也不涉及複製操作,它只是新建了一個結構放置三個數據。如下圖:
示例中,對[]int{2,3,5,7,11}
求值操作會創建一個包含五個值的數組,並設置x的屬性來描述這個數組。分割表達式x[1:3]
不重新分配記憶體數據,只寫了一個新的slice結構屬性來引用相同的存儲數據。上例中,長度為2–只有y[0]和y[1]是有效的索引,但是容量為4–y[0:4]是一個有效的分割表達式。
因為slice分割操作不需要分配記憶體,也沒有通常被保存在堆中的slice頭部,這種表示方法使slice操作和在C中傳遞指針、長度對一樣廉價。
2.2擴容
其實slice在Go的運行時庫中就是一個C語言動態數組的實現,要增加切片的容量必須創建一個新的、更大容量的切片,然後將原有切片的內容複製到新的切片。在對slice進行append等操作時,可能會造成slice的自動擴容。其擴容時的大小增長規則是:
- 如果新的大小是當前大小2倍以上,則大小增長為新大小
- 否則循環以下操作:如果當前大小小於1024,按每次2倍增長,否則每次按當前大小1/4增長。直到增長的大小超過或等於新大小。
下面的例子將切片 s
容量翻倍,先創建一個2倍 容量的新切片 t
,複製 s
的元素到 t
,然後將 t
賦值給 s
:
t := make([]byte, len(s), (cap(s)+1)*2) // +1 in case cap(s) == 0 for i := range s { t[i] = s[i] } s = t
循環中複製的操作可以由 copy 內置函數替代,返回複製元素的數目。此外, copy
函數可以正確處理源和目的切片有重疊的情況。
一個常見的操作是將數據追加到切片的尾部。必要的話會增加切片的容量,最後返回更新的切片:
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 }
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}
如果是要將一個切片追加到另一個切片尾部,需要使用 ...
語法將第2個參數展開為參數列表。
a := []string{"John", "Paul"} b := []string{"George", "Ringo", "Pete"} a = append(a, b...) // equivalent to "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 fn() 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 }
3.使用切片需要注意的陷阱
切片操作並不會複製底層的數組。整個數組將被保存在記憶體中,直到它不再被引用。 有時候可能會因為一個小的記憶體引用導致保存所有的數據。
如下, FindDigits
函數載入整個文件到記憶體,然後搜索第一個連續的數字,最後結果以切片方式返回。
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
實現一個更簡潔的版本:
8 func CopyDigitRegexp(filename string) []byte { 7 b,_ := ioutil.ReadFile(filename) 6 b = digitRefexp.Find(b) 5 var c []intb 4 // for _,v := range b{ 3 c =append(c, b) 2 //} 1 return c 0 }
4.make和new
Go有兩個數據結構創建函數:make和new,也是兩種不同的記憶體分配機制。
make和new的基本的區別是new(T)
返回一個*T
,返回的是一個指針,指向分配的記憶體地址,該指針可以被隱式地消除引用)。而make(T, args)
返回一個普通的T。通常情況下,T內部有一些隱式的指針。所以new返回一個指向已清零記憶體的指針,而make返回一個T類型的結構。更詳細的區別在後面記憶體分配的學習里研究。