go中的數據結構切片-slice

1.部分基本類型

  go中的類型與c的類似,經常使用類型有一個特例:byte類型,即字節類型,長度爲,默認值是0;git

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 個元素。數組不須要顯式的初始化;數組的零值是能夠直接使用的,數組元素會自動初始化爲其對應類型的零值。app

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中定義:ui

struct    Slice
{    // must not move anything
    byte*    array;        // actual data
    uintgo    len;        // number of elements
    uintgo    cap;        // allocated number of elements
};

  數組的slice會建立一份新的數據結構,包含一個指針,一個指針和一個容量數據。如同分割一個字符串,分割數組也不涉及複製操做,它只是新建了一個結構放置三個數據。以下圖:spa

  示例中,對[]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類型的結構。更詳細的區別在後面內存分配的學習裏研究。

相關文章
相關標籤/搜索