1. 程式人生 > >go中的資料結構切片-slice

go中的資料結構切片-slice

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型別的結構。更詳細的區別在後面記憶體分配的學習裡研究。

&n