1. 程式人生 > >GO語言slice詳解(結合原始碼)

GO語言slice詳解(結合原始碼)

一、GO語言中slice的定義

slice 是一種結構體型別,在原始碼中的定義為:

src/runtime/slice.go

type slice struct {
    array unsafe.Pointer
    len   int
    cap   int
}

從定義中可以看到,slice是一種值型別,裡面有3個元素。array是陣列指標,它指向底層分配的陣列;len是底層陣列的元素個數;cap是底層陣列的容量,超過容量會擴容。

二、初始化操作

slice有三種初始化操作,請看下面程式碼:

package main
import "fmt"
func main() {
    //1、make
    a := make([]int32, 0, 5)
    //2、[]int32{}
    b := []int32{1, 2, 3}
    //3、new([]int32)
    c := *new([]int32)
    fmt.Println(a, b, c)
}

這幾種初始化方式,在底層實現是不一樣的。有一種瞭解底層實現好的方法,就是看反彙編的呼叫函式。執行下面命令即可看到程式碼某一行的反彙編:

go tool compile -S plan9Test.go | grep plan9Test.go:行號

1、make初始化

make函式初始化有三個引數,第一個是型別,第二個長度,第三個容量,容量要大於等於長度。slice的make初始化呼叫的是底層的runtime.makeslice函式。

func makeslice(et *_type, len, cap int) slice {
    // NOTE: The len > maxElements check here is not strictly necessary,
    // but it produces a 'len out of range' error instead of a 'cap out of range' error
    // when someone does make([]T, bignumber). 'cap out of range' is true too,
    // but since the cap is only being supplied implicitly, saying len is clearer.
    // See issue 4085.
    maxElements := maxSliceCap(et.size)
    if len < 0 || uintptr(len) > maxElements {
        panic(errorString("makeslice: len out of range"))
    }

    if cap < len || uintptr(cap) > maxElements {
        panic(errorString("makeslice: cap out of range"))
    }

    p := mallocgc(et.size*uintptr(cap), et, true)
    return slice{p, len, cap}
}

主要就是呼叫mallocgc分配一塊 個數cap*型別大小 的記憶體給底層陣列,然後返回一個slice,slice的array指標指向分配的底層陣列。

2、[]int32{} 初始化

這種初始化底層是呼叫 runtime.newobject 函式直接分配相應個數的底層陣列。

// implementation of new builtin
// compiler (both frontend and SSA backend) knows the signature
// of this function
func newobject(typ *_type) unsafe.Pointer {
    return mallocgc(typ.size, typ, true)
}

3、new([]int32) 初始化

這種初始化底層也是呼叫 runtime.newobject ,new是返回slice 的地址,所以要取地址裡面內容才是真正的slice。

三、reSlice(切片操作)

所謂reSlice,是基於已有 slice 建立新的 slice 物件,以便在容量cap允許範圍內調整屬性。

data := []int32{0,1,2,3,4,5,6}
slice := data[1:4:5]  // [low:high:max]

切片操作有三個引數,low、high、max,新生成的 slice 結構體三個引數,指標array指向原slice 底層陣列元素下標為low的位置, len = high - low, cap = max - low。如下圖所示:

切片操作主要要注意的就是在原slice 容量允許範圍,超出容量範圍會報panic。

四、append 操作

slice 的 append 操作是向底層陣列尾部新增資料,返回 新的slice物件。

請看下面一段程式碼:

package main
import (
    "fmt"
)
func main() {
    a := make([]int32, 1, 2)
    b := append(a, 1)
    c := append(a, 1, 2)
    fmt.Printf("a的地址:%p, 第一個元素地址:%p,容量:%v\n", &a, &a[0], cap(a))
    //a的地址:0xc42000a060, 第一個元素地址:0xc42001a090,容量:2
    fmt.Printf("b的地址:%p, 第一個元素地址:%p,容量:%v\n", &b, &b[0], cap(b))
    //b的地址:0xc42000a080, 第一個元素地址:0xc42001a090,容量:2
    fmt.Printf("c的地址:%p, 第一個元素地址:%p,容量:%v\n", &c, &c[0], cap(c))
    //c的地址:0xc42000a0a0, 第一個元素地址:0xc42001a0a0,容量:4
}

從上面程式碼的列印結果中可以看出:a 是一個底層陣列有一個元素,容量為2的slice;append 1個元素後,沒有超出容量,產生了一個新的slice b,a 和 b 底層陣列首元素相同地址,說明a,b共用底層陣列;append 2個元素,超出了容量,產生一個新的slice c,c的底層陣列地址變了,容量也翻倍了。
那麼得出結論,append操作的執行過程:
1、如果新增資料後沒有超過原始容量,新的slice物件 和原始slice共用底層陣列,len 資料會變化,cap資料不變;
2、新增資料後超過了容量那就會擴容,重新分配一個新的底層陣列,然後拷貝底層陣列資料過去,那麼append後產生的新slice物件和原始的slice就沒有任何關係了。

擴容機制

看彙編程式碼可以知道,擴容呼叫的是底層函式 runtime.growslice
這個函式是這樣定義的:
func growslice(et *_type, old slice, cap int) slice {}
這個函式傳入三個引數:slice的原始型別,原始slice,期望的最小容量;返回一個新的slice,新slice 至少是擁有期望的最小容量,元素從原slice copy過來。

擴容規則主要是下面這段程式碼:

newcap := old.cap
    doublecap := newcap + newcap
    if cap > doublecap {
        newcap = cap
    } else {
        if old.len < 1024 {
            newcap = doublecap
        } else {
            for newcap < cap {
                newcap += newcap / 4
            }
        }
    }

擴容規則就是兩點:

  1. 如果期望的最小容量大於原始的兩倍容量時,那麼新的容量就是等於期望的最小容量;
  2. 不滿足第一種情況,那麼判斷原slice的底層陣列元素長度是不是小於1024。小於1024,新容量是原來的兩倍;大於等於1024 ,新容量是原來的1.25倍。

上面是擴容的基本規則判斷,實際上擴容還要考慮到記憶體對齊情況:

var lenmem, newlenmem, capmem uintptr
    const ptrSize = unsafe.Sizeof((*byte)(nil))
    switch et.size {
    case 1:
        lenmem = uintptr(old.len)
        newlenmem = uintptr(cap)
        capmem = roundupsize(uintptr(newcap))
        newcap = int(capmem)
    case ptrSize:
        lenmem = uintptr(old.len) * ptrSize
        newlenmem = uintptr(cap) * ptrSize
        capmem = roundupsize(uintptr(newcap) * ptrSize)
        newcap = int(capmem / ptrSize)
    default:
        lenmem = uintptr(old.len) * et.size
        newlenmem = uintptr(cap) * et.size
        capmem = roundupsize(uintptr(newcap) * et.size)
        newcap = int(capmem / et.size)
    }
記憶體對齊之後,擴容的倍數就會 >= 2 或則 1.25 了。

為什麼要記憶體對齊?

1.平臺原因(移植原因):不是所有的硬體平臺都能訪問任意地址上的任意資料的;某些硬體平臺只能在某些地址處取某些特定型別的資料,否則丟擲硬體異常。
2.效能原因:資料結構(尤其是棧)應該儘可能地在自然邊界上對齊。原因在於,為了訪問未對齊的記憶體,處理器需要作兩次記憶體訪問;而對齊的記憶體訪問僅需要一次訪問。

五、函式呼叫中實參和形參的相互影響

1、slice值傳遞,在呼叫函式中直接操作底層陣列

來看下面一段程式碼:

package main
import (
    "fmt"
)

func OpSlice(b []int32) {
    fmt.Printf("len: %d, cap: %d, data:%+v \n", len(b), cap(b), b)
    //len: 5, cap: 5, data:[1 2 3 4 5]
    fmt.Printf("b第一個元素地址:%p\n", &b[0])
    //b第一個元素地址:0xc420016120

    b[0] = 100
    fmt.Printf("len: %d, cap: %d, data:%+v \n", len(b), cap(b), b)
    //len: 5, cap: 5, data:[100 2 3 4 5]
    fmt.Printf("b第一個元素地址:%p\n", &b[0])
    //b第一個元素地址:0xc420016120
}

func main() {
    a := []int32{1, 2, 3, 4, 5}
    fmt.Printf("len: %d, cap: %d, data:%+v \n", len(a), cap(a), a)
    //len: 5, cap: 5, data:[1 2 3 4 5]
    fmt.Printf("a第一個元素地址:%p\n", &a[0])
    //a第一個元素地址:0xc420016120
    OpSlice(a)

    fmt.Printf("len: %d, cap: %d, data:%+v \n", len(a), cap(a), a)
    //len: 5, cap: 5, data:[100 2 3 4 5]
    fmt.Printf("a第一個元素地址:%p\n", &a[0])
    //a第一個元素地址:0xc420016120
}

從這段程式碼的列印中可以看到:
main函式中的slice a 是實參,值傳遞給呼叫函式時,要臨時拷貝一份給b,所以a,b 的地址是不一樣的,slice b 結構體中的三個元素都是a中的拷貝,但是元素array是指標,指標的拷貝還是指標,他們指向同一塊底層陣列,所以a,b底層陣列的第一個元素地址是一樣的。a,b共用同一塊底層陣列,在呼叫函式中,直接改變b的第一個元素內容,函式返回後a的第一個元素也變了,相當於改變了實參。

2、slice 指標傳遞

slice 指標傳遞就沒什麼說的了,在被呼叫函式中相當於操作的是實參中同一個slice,所有修改都會反映到實參。

3、slice 切片傳遞

不擴容的情況,來看下面一段程式碼:

package main
import (
    "fmt"
)
func OpSlice(b []int32) {
    fmt.Printf("len: %d, cap: %d, data:%+v \n", len(b), cap(b), b)
    //len: 3, cap: 9, data:[1 2 3]
    fmt.Printf("b第一個元素地址:%p\n", &b[0])
    //b第一個元素地址:0xc42007a064

    b = append(b, 100)
    fmt.Printf("len: %d, cap: %d, data:%+v \n", len(b), cap(b), b)
    //len: 4, cap: 9, data:[1 2 3 100]
    fmt.Printf("b第一個元素地址:%p\n", &b[0])
    //b第一個元素地址:0xc42007a064

}

func main() {
    a := []int32{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
    fmt.Printf("a第2個元素地址:%p\n", &a[1])
    //a第2個元素地址:0xc42007a064

    fmt.Printf("len: %d, cap: %d, data:%+v \n", len(a), cap(a), a)
    //len: 10, cap: 10, data:[0 1 2 3 4 5 6 7 8 9]
    fmt.Printf("a第一個元素地址:%p\n", &a[0])
    //a第一個元素地址:0xc42007a060
    OpSlice(a[1:4])

    fmt.Printf("len: %d, cap: %d, data:%+v \n", len(a), cap(a), a)
    //len: 10, cap: 10, data:[0 1 2 3 100 5 6 7 8 9]
    fmt.Printf("a第一個元素地址:%p\n", &a[0])
    //a第一個元素地址:0xc42007a060

}

前面已經講過,切片和原slice是共用底層陣列的。不擴容情況下,對切片產生的新的slice append 操作,新增加的元素會新增到底層陣列尾部,會覆蓋原有的值,反映到原slice中去;

總結

無論是slice的什麼操作:拷貝,append,reSlice 等等都會產生新的slice,但是他們是共用底層陣列的,不擴容情況,他們增刪改元素都會影響到原來的slice底層陣列;擴容情況下,產生的是一個“真正的”新的slice物件,和原來的完全獨立開了,底層陣列完全不會影響。

參考資料

  1. 深度解密Go語言之Slice.
  2. Go語言學習筆記.