golang slice 使用及源碼分析

 

  • 1.先作個小實驗
func main(){
    s1:=make([]int,0,10)
    s1=[]int{1,2,3}
    ss:=make([]int,0,10)
    ss = s1[1:]
    for i:=0;i<len(ss);i++{
        ss[i] +=10
    }
    fmt.Println(s1)   // [1 12 13]
    ss =append(ss,4)
    for i:=0;i<len(ss);i++{
        ss[i] +=10
    }
    fmt.Println(s1)  // [1 12 13] 而不是 [1,22,23]

    t:=[]int{0}
    printPoint(t)  // 0xc4200140a8 cap(s)= 1
    t = append(t,1)
    printPoint(t)  // 0xc4200140c0 0xc4200140c8 cap(s)= 2
    t = append(t,2)
    printPoint(t)   // 0xc4200160e0 0xc4200160e8 0xc4200160f0 cap(s)= 4
    t = append(t,3)
    printPoint(t)   //  0xc4200160e0 0xc4200160e8 0xc4200160f0 0xc4200160f8 cap(s)= 4
}

func printPoint(s []int){
    for i:=0;i<len(s);i++{
        fmt.Print(unsafe.Pointer(&s[i])," ")

    }
    fmt.Println("cap(s)=",cap(s))
}

發現slice在進行append操做時會跟據原來的slice容量,若是append完成後新slice的容量超過原來slice的容量,則須要擴容,而且將舊的slice數據所有遷移到新的slice開闢的地址裏。數組

  • 2.在runtime目錄下找到slice.go,定位到growslice(et *_type, old slice, cap int)這個函數
type slice struct {
    array unsafe.Pointer
    len   int
    cap   int
}

// growslice handles slice growth during append.
// It is passed the slice element type, the old slice, and the desired new minimum capacity,
// and it returns a new slice with at least that capacity, with the old data
// copied into it.
// The new slice's length is set to the old slice's length,
// NOT to the new requested capacity.
// This is for codegen convenience. The old slice's length is used immediately
// to calculate where to write new values during an append.
// TODO: When the old backend is gone, reconsider this decision.
// The SSA backend might prefer the new length or to return only ptr/cap and save stack space.

// 與append(slice,s)對應的函數growslice
// 經過切片的類型,舊切片的容量和數據得出新切片的容量,新切片跟據容量從新申請一塊地址,把舊切片的數據拷貝到新切片中

func growslice(et *_type, old slice, cap int) slice {

// 單純地擴容,不寫數據
    if et.size == 0 {
        if cap < old.cap {
            panic(errorString("growslice: cap out of range"))
        }
        // append should not create a slice with nil pointer but non-zero len.
        // We assume that append doesn't need to preserve old.array in this case.
        return slice{unsafe.Pointer(&zerobase), old.len, cap}
    }
// 擴容規則 1.新的容量大於舊的2倍,直接擴容至新的容量
// 2.新的容量不大於舊的2倍,當舊的長度小於1024時,擴容至舊的2倍,不然擴容至舊的5/4倍
    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
            }
        }
    }

// 跟據切片類型和容量計算要分配內存的大小
    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)
    }

// 異常狀況,舊的容量比新的容量還大或者新的容量超過限制了
    if cap < old.cap || uintptr(newcap) > maxSliceCap(et.size) {
        panic(errorString("growslice: cap out of range"))
    }

    var p unsafe.Pointer
    if et.kind&kindNoPointers != 0 {

// 爲新的切片開闢容量爲capmem的地址空間
        p = mallocgc(capmem, nil, false)
// 將舊切片的數據搬到新切片開闢的地址中
        memmove(p, old.array, lenmem)
        // The append() that calls growslice is going to overwrite from old.len to cap (which will be the new length).
        // Only clear the part that will not be overwritten.
// 清理下新切片中剩餘地址,不能存放堆棧指針

// memclrNoHeapPointers clears n bytes starting at ptr.
//
// Usually you should use typedmemclr. memclrNoHeapPointers should be
// used only when the caller knows that *ptr contains no heap pointers
// because either:
//
// 1. *ptr is initialized memory and its type is pointer-free.
//
// 2. *ptr is uninitialized memory (e.g., memory that's being reused
//    for a new allocation) and hence contains only "junk".
        memclrNoHeapPointers(add(p, newlenmem), capmem-newlenmem)
    } else {
        // Note: can't use rawmem (which avoids zeroing of memory), because then GC can scan uninitialized memory.
        p = mallocgc(capmem, et, true)
        if !writeBarrier.enabled {
            memmove(p, old.array, lenmem)
        } else {
            for i := uintptr(0); i < lenmem; i += et.size {
                typedmemmove(et, add(p, i), add(old.array, i))
            }
        }
    }

    return slice{p, old.len, newcap}
}

 

  •  3.slice做爲函數參數
func main(){
    s:=make([]int,0,5)
    s=append(s,1,2,3,4)
    printPoint(s)   // 1 0xc420018120 2 0xc420018128 3 0xc420018130 4 0xc420018138 cap(s)= 5 &s= 0xc42000a060
    processSlice(s)  //11 0xc420018120 12 0xc420018128 13 0xc420018130 14 0xc420018138 11 0xc420018140 cap(s)= 5 &s= 0xc42000a080
}

func processSlice(ss []int){
    for i:=0;i<len(ss);i++{
        ss[i] +=10
    }
    ss=append(ss,11)
    printPoint(ss)
}
func printPoint(s []int){
    for i:=0;i<len(s);i++{
        fmt.Print(s[i],unsafe.Pointer(&s[i])," ")

    }
    fmt.Println("cap(s)=",cap(s),"&s=",unsafe.Pointer(&s))
}

   函數中的形參slice是實參的拷貝,指向切片的指針不一樣,因爲sice沒有擴容,函數裏面的slice和主函數的實參slice指向的數組地址是同樣的app

func main(){
    s:=make([]int,0,4)
    s=append(s,1,2,3,4)
    printPoint(s)  // 1 0xc42008c000 2 0xc42008c008 3 0xc42008c010 4 0xc42008c018 cap(s)= 4 &s= 0xc42008a020
    processSlice(s) // 11 0xc420092000 12 0xc420092008 13 0xc420092010 14 0xc420092018 11 0xc420092020 cap(s)= 8 &s= 0xc42008a040

}

func processSlice(ss []int){
    for i:=0;i<len(ss);i++{
        ss[i] +=10
    }
    ss=append(ss,11)
    printPoint(ss)
}
func printPoint(s []int){
    for i:=0;i<len(s);i++{
        fmt.Print(s[i],unsafe.Pointer(&s[i])," ")

    }
    fmt.Println("cap(s)=",cap(s),"&s=",unsafe.Pointer(&s))
}

函數中的形參slice是實參的拷貝,指向切片的指針不一樣,因爲sice擴容了,函數裏面的slice和主函數的實參slice指向的數組地址是不同的ide

 

  • 4.總結
  1. 不要輕易的對切片append,若是新的切片容量比舊的大的話,須要進行growslice操做,新的地址開闢,數據拷貝
  2. 儘可能對切片設置初始容量值以免growslice,相似make([]int,0,100)
  3. 切片是一個結構體,保存着切片的容量,實際長度以及數組的地址
  4. 切片做爲函數參數傳入會進行引用拷貝,生成一個新的切片,指向同一個數組
相關文章
相關標籤/搜索