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

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

作者 | 馬超

責編 | 夕顏

封圖 | CSDN下載自視覺中國

出品 | CSDN(ID:CSDNnews)

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

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

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

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

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

GO語言的切片簡介

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

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

GO語言的代碼解讀

1. append函數添加元素

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

<code>

package

main/<code>
<code>import (/<code><code>

"fmt"

/<code><code>)/<code>
<code>

func

main

{/<code>
<code>

var

a

int

/<code><code>/<code><code>/<code><code>/<code><code>/<code><code>/<code><code>/<code><code>/<code>
<code>/<code>

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

2.在切片中元素的刪除

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

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

具體代碼如下:

<code>

package

main

/<code>

<code>

import

(

/<code><code>

"fmt"

/<code><code>

)

/<code>
<code>

func

main

{

/<code><code>

var

a

=

int{1,

2

,

3

,

4

,

5

,

6

,

7

,

8

,

9

,

10

}

//使用原始定義法來聲明並初始化一個切片

/<code><code>

fmt.Println(a)

//運行結果為[1

2

3

4

5

6

7

8

9

10

]

/<code><code>

a

=

a[1:]

//

刪除第1個元素

/<code><code>

fmt.Println(a)

//刪頭第1個元素後,運行結果為[2

3

4

5

6

7

8

9

10

]

/<code><code>

a

=

a[2:]

//

刪除前2個元素

/<code><code>

fmt.Println(a)

//刪頭前2個元素後,運行結果為[4

5

6

7

8

9

10

]

/<code>
<code>

}

/<code>

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

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

1.slice 結構定義

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

<code>

type

slice

struct

{/<code>
<code>array unsafe.Pointer /<code><code>

len

int

/<code><code>

cap

int

/<code><code>}/<code>

2.創建切片的makeslice函數

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

<code>func makeslice(et *_type, 

len

, cap int) slice {/<code>
<code> mem, overflow :=

math

.MulUintptr(et.size, uintptr(cap))//此函數計算et.size也就是每個元素所佔空間的大小,並與容量cap相乘,其中mem既為所需要最大內存,overflow代表是否會造成溢出/<code><code>

if

overflow || mem > maxAlloc ||

len

0

||

len

> cap {//判斷是否有溢出,長度為負數或者長度比容量大的情況,如存在 直接panic/<code><code>// NOTE: Produce a

'len out of range'

error

instead of a/<code><code>//

'cap out of range'

error

when someone does make([]T, bignumber)./<code><code>//

'cap out of range'

is

true

too, but since the cap is only being/<code><code>// supplied implicitly, saying

len

is clearer./<code><code>// See golang.org/issue/

4085.

/<code><code>mem, overflow :=

math

.MulUintptr(et.size, uintptr(

len

))/<code><code>

if

overflow || mem > maxAlloc ||

len

0

{/<code><code>panicmakeslicelen/<code><code>}/<code><code>panicmakeslicecap/<code><code>}/<code><code>

return

mallocgc(mem, et,

true

)// 如果錯誤檢查成功,則分配內存,注意slice對象會被GC所自動清除。/<code>
<code>}/<code>

3.擴容函數growslice

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

<code>func growslice(et *_type, old slice, cap int) slice {/<code>

<code>

//

單純地擴容,不寫數據/<code><code>

if

et.size ==

0

{/<code><code>

if

cap < old.cap {/<code><code> panic(errorString(

"growslice: cap out of range"

))/<code><code> }/<code><code> /

/ append should not create a slice with nil pointer but non-zero len.

/<code><code>

/

/ We assume that append doesn't need to preserve old.array in this case.

/<code><code>

return slice{unsafe.Pointer(&zerobase), old.len, cap}

/<code><code>

}

/<code><code>

/

/ 擴容規則 1.新的容量大於舊的2倍,直接擴容至新的容量

/<code><code>

/

/ 2.新的容量不大於舊的2倍,當舊的長度小於1024時,擴容至舊的2倍,否則擴容至舊的1.25倍

/<code><code>

newcap := old.cap

/<code><code>

doublecap := newcap + newcap

/<code><code>

if cap > doublecap {

/<code><code>

newcap = cap

/<code><code>

} else {

/<code><code>

if old.len

< 1024 {

/<code><code>

newcap = doublecap

/<code><code>

} else {

/<code><code>

for newcap < cap {

/<code><code>

newcap += newcap /

4

/<code><code> }/<code><code> }/<code><code> }/<code>
<code>/

/ 跟據切片類型和容量計算要分配內存的大小

/<code>
<code>

var overflow bool

/<code><code>

var lenmem, newlenmem, capmem uintptr

/<code><code>

switch {

/<code><code>

case et.size == 1:

/<code><code>

lenmem = uintptr(old.len)

/<code><code>

newlenmem = uintptr(cap)

/<code><code>

capmem = roundupsize(uintptr(newcap))

/<code><code>

overflow = uintptr(newcap) > maxAlloc

/<code><code>

newcap = int(capmem)

/<code><code>

case et.size == sys.PtrSize:

/<code><code>

lenmem = uintptr(old.len) * sys.PtrSize

/<code><code>

newlenmem = uintptr(cap) * sys.PtrSize

/<code><code>

capmem = roundupsize(uintptr(newcap) * sys.PtrSize)

/<code><code>

overflow = uintptr(newcap) > maxAlloc/sys

.PtrSize/<code><code>newcap = int(capmem / sys.PtrSize)/<code><code>

case

isPowerOfTwo(et.size)

:

/<code><code>

var

shift uintptr/<code><code>if sys.PtrSize ==

8

{/<code><code>

//

Mask shift

for

better code generation./<code><code>shift = uintptr(sys.Ctz64(uint64(et.size))) &

63

/<code><code>}

else

{/<code><code>shift = uintptr(sys.Ctz32(uint32(et.size))) &

31

/<code><code>}/<code><code>lenmem = uintptr(old.len) << shift/<code><code>newlenmem = uintptr(cap) << shift/<code><code>capmem = roundupsize(uintptr(newcap) << shift)/<code><code>overflow = uintptr(newcap) > (maxAlloc >> shift)/<code><code>newcap = int(capmem >> shift)/<code><code>

default:

/<code><code>lenmem = uintptr(old.len) * et.size/<code><code>newlenmem = uintptr(cap) * et.size/<code><code>capmem, overflow = math.MulUintptr(et.size, uintptr(newcap))/<code><code>capmem = roundupsize(capmem)/<code><code>newcap = int(capmem / et.size)/<code><code>}/<code><code>/

/ 異常情況,舊的容量比新的容量還大或者新的容量超過限制了

/<code><code>

if cap < old.cap || uintptr(newcap) > maxSliceCap(et.size) {

/<code><code>

panic(errorString("growslice: cap out of range"))

/<code><code>

}

/<code>
<code>

var p unsafe.Pointer

/<code><code>

if et.kind&kindNoPointers != 0 {

/<code>
<code>

/

/ 為新的切片開闢容量為capmem的地址空間

/<code><code>

p = mallocgc(capmem, nil, false)

/<code><code>

/

/ 將舊切片的數據搬到新切片開闢的地址中

/<code><code>

memmove(p, old.array, lenmem)

/<code><code>

/

/ The append that calls growslice is going to overwrite from old.len to cap (which will be the new length).

/<code><code>

/

/ Only clear the part that will not be overwritten.

/<code><code>

/

/ 清理下新切片中剩餘地址,不能存放堆棧指針

/<code>
<code>

/

/ memclrNoHeapPointers clears n bytes starting at ptr.

/<code><code>

/

/

/<code><code>

/

/ Usually you should use typedmemclr. memclrNoHeapPointers should be

/<code><code>

/

/ used only when the caller knows that *ptr contains no heap pointers

/<code><code>

/

/ because either:

/<code><code>

/

/

/<code><code>

/

/ 1. *ptr is initialized memory and its type is pointer-free.

/<code><code>

/

/

/<code><code>

/

/ 2. *ptr is uninitialized memory (e.g., memory that's being reused

/<code><code>

/

/ for a new allocation) and hence contains only "junk".

/<code><code>

memclrNoHeapPointers(add(p, newlenmem), capmem-newlenmem)

/<code><code>

} else {

/<code><code>

/

/ Note: can't use rawmem (which avoids zeroing of memory), because then GC can scan uninitialized memory.

/<code><code>

p = mallocgc(capmem, et, true)

/<code><code>

if !writeBarrier.enabled {

/<code><code>

memmove(p, old.array, lenmem)

/<code><code>

} else {

/<code><code>

for i := uintptr(0); i < lenmem; i += et.size {

/<code><code>

typedmemmove(et, add(p, i), add(old.array, i))

/<code><code>

}

/<code><code>

}

/<code><code>

}

/<code>
<code>

return slice{p, old.len, newcap}

/<code><code>

}

/<code>
為什麼大廠都在用 GO 語言?讀透 GO 語言的切片

GO語言切片的相關結論

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

  1. append方式為數據增加元素時,如果觸發切片進行擴容,則肯定是新生成了一個切片對象,並且涉及內存操作,因此append操作一定要小心。

  2. 建議儘量通過make函數來聲明一個切片,並在初始時儘量設定好一個合理的容量值,避免切片頻繁擴容帶來不必要的開銷。

原文鏈接:

https://blog.csdn.net/BEYONDMA/article/details/104799500

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

今日福利

遇見陸奇

同樣作為“百萬人學 AI”的重要組成部分,2020 AIProCon 開發者萬人大會將於 7 月 3 日至 4 日通過線上直播形式,讓開發者們一站式學習瞭解當下 AI 的前沿技術研究、核心技術與應用以及企業案例的實踐經驗,同時還可以在線參加精彩多樣的開發者沙龍與編程項目。參與前瞻系列活動、在線直播互動,不僅可以與上萬名開發者們一起交流,還有機會贏取直播專屬好禮,與技術大咖連麥。


分享到:


相關文章: