編程學習網 > 編程語言 > go > 為什么大廠都在用 GO 語言?讀透 GO 語言的切片
2020
05-07

為什么大廠都在用 GO 語言?讀透 GO 語言的切片



今年3月初,騰訊發布了《騰訊研發大數據報告》,筆者發現GO語言的使用在鵝廠已經上升到了TOP5的位置了。

 


我們知道騰訊尤其是Docker容器化這一塊,是走在各大廠的前列的,尤其是他們的基于GO語言開發的DEVOPS藍鯨平臺,水平相當高。


經筆者實地上手體驗,GO語言在并發等方面還是相當優秀的,下面筆者就匯報一下最新的成果。



一、GO語言的切片簡介


切片(slice)是對數組的一個連續片段的引用,所以切片是一個引用類型同,與Python 中的 list 類型比較類似,這個片段可以是整個數組,也可以是由起始和終止索引標識的一些項的子集。Go語言中切片的內部結構包含地址、大小(len)和容量(cap)與數組相比切片最大的特點就是其容量是可變的。



二、GO語言的代碼解讀


1. append函數添加元素


Go語言的內建函數 append() 可以為切片動態添加元素,不過需要注意的是,由于切片本身是變長的,因此在使用 append() 函數為切片動態添加元素時,切片就會自動進行“擴容”,同時新切片的長度也會增加,但是有一點需要注意,append返回的是一個新的切片對象,而不是對原切片進行操作。在下面的代碼中我們先定義了一個切片a,并不斷通過append方式為其增加元素,并觀察切片a的長度及容量變化。


package main

import (
"fmt"
)

func main() {

var a []int //定義一個切片
fmt.Printf("len: %d  cap: %d pointer: %p\n", len(a), cap(a), a)//此時切片長度和容量都是0,運行結果為len: 0  cap: 0 pointer: 0x0
a = append(a, 1) // 追加1個元素
fmt.Printf("len: %d  cap: %d pointer: %p\n", len(a), cap(a), a)//注意此時a的地址已經發生變化為新的切片了,新切片長度和容量都為1運行結果為:len: 1  cap: 1 pointer: 0xc000072098
a = append(a, 2, 3, 4) // 追加多個元素
fmt.Printf("len: %d  cap: %d pointer: %p\n", len(a), cap(a), a)//注意此時a的地址再次發生變化實際上又生成為新的切片了,新切片長度和容量都為4運行結果為:len: 4  cap: 4 pointer: 0xc000070160
a = append(a, 5) // 再追加一個元素
fmt.Printf("len: %d  cap: %d pointer: %p\n", len(a), cap(a), a)//注意切片擴容策略是倍增方式容量由4變成8,而長度是5運行結果為:len: 4  cap: 4 pointer: 0xc000070160

}


可以觀察到切片在擴容時,其容量(cap)的速度規律是以2 倍數進行的。


2.在切片中元素的刪除


刪除切片中開頭的N個元素


使用x = x[N:] 的方式來在切片中刪除由第i個元素開始的N個元素

具體代碼如下:

package main


import (
"fmt"
)

func main() {
var a = []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10} //使用原始定義法來聲明并初始化一個切片
fmt.Println(a)                               //運行結果為[1 2 3 4 5 6 7 8 9 10]
a = a[1:]                                    // 刪除第1個元素
fmt.Println(a)                               //刪頭第1個元素后,運行結果為[2 3 4 5 6 7 8 9 10]
a = a[2:]                                    // 刪除前2個元素
fmt.Println(a)                               //刪頭前2個元素后,運行結果為[4 5 6 7 8 9 10]

}


3、深入理解GO語言中的切片


有關切片的代碼位置在GOPATH\src\runtime\slice.go,其中對于幾個重點函數解讀如下:


1.slice 結構定義


首先slice是這樣一個結構體,他有一個存放數據的數組,和一個長度len與容量cap構成


type slice struct {

array unsafe.Pointer    
len   int    
cap   int
}


2.創建切片的makeslice函數


而創建切片的函數makeslice如下,可以看到函數會對于內存進行預分配,如果成功再正式分配內存,他建切片的makeslice函數源碼及注釋如下:


func makeslice(et *_type, len, cap int) slice {

     mem, overflow := math.MulUintptr(et.size, uintptr(cap))//此函數計算et.size也就是每個元素所占空間的大小,并與容量cap相乘,其中mem既為所需要最大內存,overflow代表是否會造成溢出
if overflow || mem > maxAlloc || len < 0 || len > cap {//判斷是否有溢出,長度為負數或者長度比容量大的情況,如存在 直接panic
// NOTE: Produce 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 golang.org/issue/4085.
mem, overflow := math.MulUintptr(et.size, uintptr(len))
if overflow || mem > maxAlloc || len < 0 {
panicmakeslicelen()
}
panicmakeslicecap()
}
return mallocgc(mem, et, true)// 如果錯誤檢查成功,則分配內存,注意slice對象會被GC所自動清除。

}


3.擴容函數growslice


通過閱讀growslice的源碼可以看在這個函數當中,擴容的規則是在長度小于1024時按照一直采用的是翻倍的方式進行擴容,在大于1024后,每次擴容至原容量的1.25倍,新容量計算完成后對于內存進行預分配,這點也makeslice的想法一致,接下再將老slice中的數據通過memmove(p, old.array, lenmem)的方式拷貝至新的slice。growlice函數源碼及注釋如下:


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倍,否則擴容至舊的1.25倍
    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 overflow bool
var lenmem, newlenmem, capmem uintptr
    switch {
case et.size == 1:
lenmem = uintptr(old.len)
newlenmem = uintptr(cap)
capmem = roundupsize(uintptr(newcap))
overflow = uintptr(newcap) > maxAlloc
newcap = int(capmem)
case et.size == sys.PtrSize:
lenmem = uintptr(old.len) * sys.PtrSize
newlenmem = uintptr(cap) * sys.PtrSize
capmem = roundupsize(uintptr(newcap) * sys.PtrSize)
overflow = uintptr(newcap) > maxAlloc/sys.PtrSize
newcap = int(capmem / sys.PtrSize)
case isPowerOfTwo(et.size):
var shift uintptr
if sys.PtrSize == 8 {
// Mask shift for better code generation.
shift = uintptr(sys.Ctz64(uint64(et.size))) & 63
} else {
shift = uintptr(sys.Ctz32(uint32(et.size))) & 31
}
lenmem = uintptr(old.len) << shift
newlenmem = uintptr(cap) << shift
capmem = roundupsize(uintptr(newcap) << shift)
overflow = uintptr(newcap) > (maxAlloc >> shift)
newcap = int(capmem >> shift)
default:
lenmem = uintptr(old.len) * et.size
newlenmem = uintptr(cap) * et.size
capmem, overflow = math.MulUintptr(et.size, uintptr(newcap))
capmem = roundupsize(capmem)
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}
}


三、GO語言切片的相關結論


所以通過閱讀以上源代碼我們也可以知道,有以下兩點結論:

  • append方式為數據增加元素時,如果觸發切片進行擴容,則肯定是新生成了一個切片對象,并且涉及內存操作,因此append操作一定要小心。
  • 建議盡量通過make函數來聲明一個切片,并在初始時盡量設定好一個合理的容量值,避免切片頻繁擴容帶來不必要的開銷。

掃碼二維碼 獲取免費視頻學習資料

Python編程學習

查 看2022高級編程視頻教程免費獲取