go語言中的數組切片:特立獨行的可變數組

初看go語言中的slice,以爲是可變數組的一種很不錯的實現,直接在語言語法的層面支持,操做方面比起java中的ArrayList方便了許多。可是在使用了一段時間後,以爲這東西埋的坑很多,使用方式上和arrayList也有很大的不一樣,在使用時要格外注意。java

slice的數據結構

首先說一下slice的數據結構,源碼能夠在google code上找到,http://code.google.com/p/go/source/browse/src/pkg/runtime/runtime.hgit

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

能夠看出主要保存了三個信息:github

  • 一個指向原生數組的指針
  • 元素的個數
  • 數組分配的存儲空間

slice的基本操做

go中生成切片的方式有如下幾種,這幾種生成方式也對應了對slice的基本操做,每一個操做後面go隱藏了不少的細節,若是沒有對其足夠了解,在使用時很容易被這些坑絆倒。數組

1.make函數生成數據結構

這是最基本,最原始生成slice切片的方式,經過其餘方式生成的切片最終也是經過這種方式來完成。由於不管如何都須要填充上面slice結構的三個最基本信息。app

經過查找源碼,發現最終都是通過下面的c代碼實現的:函數

static void makeslice1(SliceType *t, intgo len, intgo cap, Slice *ret)
{
    ret->len = len;
    ret->cap = cap;
    ret->array = runtime·cnewarray(t->elem, cap);
}

make函數在生成slice時的寫法:post

var slice1 = make([]int, 0, 5)
var slice2 = make([]int, 5, 5)
// 省略len的寫法,len默認等於cap,至關於make([]int, 5, 5)
var slice3 = make([]int, 5)

這個簡便的寫法實在是有點坑爹,若是你寫成make([]int, 5),go會默認把數組長度len看成slice的容量,按照上面的例子,便生成了這樣的結構:[0 0 0 0 0]性能

2.對數組進行切片 首先來看下面的代碼:ui

arr := [5]int{1, 2, 3, 4, 5}
slice := arr[3 : 5] //  slice:[4, 5]
slice[0] = 0        // slice:[0, 5]
fmt.Println(slice)
fmt.Println(arr)

輸出結果:

[0 5]
[1 2 3 0 5]

從上面能夠看出,對數組進行了切片操做,生成的切片裏的array指針實際指向了原數組的一個位置,至關於c的代碼中對原數組截取生成新的數組[2]arrNew,數組的指針指向arr[3],因此改變切片裏0下標對應元素的值,實際上也就改變了原數組相應數組位置3中元素的值。

關於這個問題這篇博文說的比較詳細:對Go的Slice進行Append的一個「坑」

3.對數組或切片進行append

我的認爲這個append是go語言中實現地不太優雅的一個地方,好比對一個slice進行append必需要這樣寫:slice = append(slice, 1)。說白了就是,對一個slice進行append時,必須把新的引用從新賦值給slice。若是隻是語法上怪異,那問題還好,只是代碼寫起來麻煩一點。可是實際狀況是這個append操做致使的問題多多,不當心很容易走到append埋的坑裏面去。

先來看一個比較奇怪的現象:

var sliceA = make([]int, 0, 5)
sliceB := append(sliceA, 1)
fmt.Println(sliceA)
fmt.Println(sliceB)

輸出結果是:

[]
[1]

剛看到這樣的結果時讓人很難以理解,明明聲明瞭容量是5的切片,如今sliceA的len是0,遠沒有達到切片的容量。按理說對sliceA進行append操做,在沒有達到切片容量的狀況下根本不須要從新申請一個新的大容量的數組,只須要在本來數組內修改元素的值。並且,go函數在傳輸切片時是引用傳遞,這樣的話,sliceB和sliceA應該輸出同樣纔對。看到這樣的結果,着實讓人困惑了很長時間,難道每次append操做都會從新分配數組嗎?

答案確定不是這樣的,若是真是這樣的話,go也就不用再混了,性能確定會出問題。下面從go實現append的源碼中去找答案,源碼位置在:http://code.google.com/p/go/source/browse/src/pkg/runtime/slice.c 代碼很長,這裏只截取關鍵的片斷來講明問題:

void runtime·appendslice(SliceType *t, Slice x, Slice y, Slice ret)
{
    intgo m = x.len+y.len;
    void *pc;
    if(m > x.cap)
        growslice1(t, x, m, &ret);
    else
        ret = x;
    // read x[:len]
    if(m > x.cap)
        runtime·racereadrangepc(x.array, x.len*w, pc, runtime·appendslice);
    // read y
    runtime·racereadrangepc(y.array, y.len*w, pc, runtime·appendslice);
    // write x[len(x):len(x)+len(y)]
    if(m <= x.cap)
        runtime·racewriterangepc(ret.array+ret.len*w, y.len*w, pc, runtime·appendslice);
    ret.len += y.len;
    FLUSH(&ret);
}

函數定義appendslice(SliceType *t, Slice x, Slice y, Slice ret),對應slice3 = append(slice1, slice1...)操做,分別表明:數組裏的元素類型、slice1, slice2, slice3。雖然append()語法中,第二個參數不能爲slice,可是第二個參數實際上是一個可變參數elems ...Type,能夠傳輸打散的數組,因此go在處理時一樣是轉換爲slice來操做的。

從上面的代碼很清楚的看到,若是x.len + y.len 超過了x.cap,那麼就會從新擴展新的切片,若是x.len + y.len尚未超過x.cap,則仍是在原切片的數組中進行元素的填充。那麼這樣跟咱們理性的認識是一致的。能夠打消掉以前誤解的對go append的擔憂。那問題出在哪呢?

上面忽略了一點,append函數是有go的代碼的,不是直接語言級c的實現,在c的實現上還加了go語言本身的處理,在/pkg/builtin/bulitin.go裏有函數的定義。這裏我只能假設在go的層面對scliceA作了一些隱祕的處理,go如何去調用c的底層實現,我如今還不甚瞭解,這裏也只能分析到這裏。之後瞭解以後再來補充這篇博客,若是有了解的朋友,也很是感激你告訴我。

4.聲明無長度的數組

聲明無長度的數組其實就是聲明瞭一個可變數組,也就是slice切片。只不過這個切片的len和cap都是0。這個方法寫起來很是方便,若是不瞭解其背後的實現,那麼這樣用起來是性能最差的一種。由於會致使頻繁的對slice進行從新申請內容的操做,而且須要把,原數組中的元素copy到新的大容量的數組裏去。每次從新分配數組容量的步長是len*2,若是進行n次append,那麼須要通過log2(n)次的從新申請內存和copy的開銷。

後面的一篇文章會繼續介紹切片和數組的一些區別:

go slice和數組的區別

 

還能夠訪問我樹莓派上搭的博客地址:

http://www.codeforfun.info/

相關文章
相關標籤/搜索