從源碼開始分析Go語言的切片

本文約3439字,建議收藏後閱讀。

我的個人博客:www.godeamon.com。博客看代碼更加方便。


從源碼開始分析Go語言的切片

Go


1.切片是啥

Go 的 Slice(切片)類型提供了一種方便有效的方法來處理類型化數據序列。slice 與其他語言的數組類似卻又不同,簡單來說,切片更加靈活,用起來更方便,其原因就是可以擴容。

2.舉例分析

2.1. 認識切片第一步

<code>

package

main

import

"fmt"

func

main

()

{ sliceExample() }

func

sliceExample

()

{ slc :=

make

([]

int

,

0

) slc =

append

(slc,

1

) fmt.Println(slc)

var

slc1 []

int

slc1 =

append

(slc1,

1

) fmt.Println(slc1)

var

slc2 []

int

slc2 =

append

(slc2,

1

) fmt.Println(slc2) }/<code>

上面的代碼是最基本的使用方式,首先看一下上面的代碼在底層都做了哪些東西(不要害怕彙編,其實很簡單,我們主要明白部分彙編代碼即可)

命令:go tool compile -S slice.go 我的 go 文件為 main/slice.go

<code>

""

.main

STEXT

size=48

args=0x0

locals=0x8

0x0000

00000

(slice.go:5)

TEXT

""

.main(SB),

ABIInternal,

$8-0

......

0x0021

00033

(slice.go:10)

PCDATA

$2,

$1

0x0021

00033

(slice.go:10)

PCDATA

$0,

$0

0x0021

00033

(slice.go:10)

LEAQ

type.int(SB),

AX

0x0028

00040

(slice.go:10)

PCDATA

$2,

$0

0x0028

00040

(slice.go:10)

MOVQ

AX,

(SP)

0x002c

00044

(slice.go:10)

XORPS

X0,

X0

0x002f

00047

(slice.go:10)

MOVUPS

X0,

8

(SP)

0x0034

00052

(slice.go:10)

CALL

runtime.makeslice(SB)

0x0039

00057

(slice.go:10)

PCDATA

$2,

$1

0x0039

00057

(slice.go:10)

MOVQ

24

(SP),

AX

0x003e

00062

(slice.go:11)

PCDATA

$2,

$2

0x003e

00062

(slice.go:11)

LEAQ

type.int(SB),

CX

0x0045

00069

(slice.go:11)

PCDATA

$2,

$1

0x0045

00069

(slice.go:11)

MOVQ

CX,

(SP)

0x0049

00073

(slice.go:11)

PCDATA

$2,

$0

0x0049

00073

(slice.go:11)

MOVQ

AX,

8

(SP)

0x004e

00078

(slice.go:11)

XORPS

X0,

X0

0x0051

00081

(slice.go:11)

MOVUPS

X0,

16

(SP)

0x0056

00086

(slice.go:11)

MOVQ

$1,

32

(SP)

0x005f

00095

(slice.go:11)

CALL

runtime.growslice(SB)

0x0064

00100

(slice.go:11)

PCDATA

$2,

$1

0x0064

00100

(slice.go:11)

MOVQ

40

(SP),

AX

0x0069

00105

(slice.go:11)

MOVQ

48

(SP),

CX

0x006e

00110

(slice.go:11)

MOVQ

56

(SP),

DX

0x0073

00115

(slice.go:11)

MOVQ

$1,

(AX)

0x007a

00122

(slice.go:12)

PCDATA

$2,

$0

0x007a

00122

(slice.go:12)

MOVQ

AX,

(SP)

0x007e

00126

(slice.go:11)

LEAQ

1

(CX),

AX

0x0082

00130

(slice.go:12)

MOVQ

AX,

8

(SP)

0x0087

00135

(slice.go:12)

MOVQ

DX,

16

(SP)

0x008c

00140

(slice.go:12)

CALL

runtime.convTslice(SB)

......

0x00e8

00232

(slice.go:15)

PCDATA

$2,

$1

0x00e8

00232

(slice.go:15)

LEAQ

type.int(SB),

AX

0x00ef

00239

(slice.go:15)

PCDATA

$2,

$0

0x00ef

00239

(slice.go:15)

MOVQ

AX,

(SP)

0x00f3

00243

(slice.go:15)

XORPS

X0,

X0

0x00f6

00246

(slice.go:15)

MOVUPS

X0,

8

(SP)

0x00fb

00251

(slice.go:15)

MOVQ

$0,

24

(SP)

0x0104

00260

(slice.go:15)

MOVQ

$1,

32

(SP)

0x010d

00269

(slice.go:15)

CALL

runtime.growslice(SB)

0x0112

00274

(slice.go:15)

PCDATA

$2,

$1

0x0112

00274

(slice.go:15)

MOVQ

40

(SP),

AX

0x0117

00279

(slice.go:15)

MOVQ

48

(SP),

CX

0x011c

00284

(slice.go:15)

MOVQ

56

(SP),

DX

0x0121

00289

(slice.go:15)

MOVQ

$1,

(AX)

0x0128

00296

(slice.go:16)

PCDATA

$2,

$0

0x0128

00296

(slice.go:16)

MOVQ

AX,

(SP)

0x012c

00300

(slice.go:15)

LEAQ

1

(CX),

AX

0x0130

00304

(slice.go:16)

MOVQ

AX,

8

(SP)

0x0135

00309

(slice.go:16)

MOVQ

DX,

16

(SP)

0x013a

00314

(slice.go:16)

CALL

runtime.convTslice(SB)

......

0x0196

00406

(slice.go:18)

PCDATA

$2,

$1

0x0196

00406

(slice.go:18)

LEAQ

type.[0]int(SB),

AX

0x019d

00413

(slice.go:18)

PCDATA

$2,

$0

0x019d

00413

(slice.go:18)

MOVQ

AX,

(SP)

0x01a1

00417

(slice.go:18)

CALL

runtime.newobject(SB)

0x01a6

00422

(slice.go:19)

PCDATA

$2,

$1

0x01a6

00422

(slice.go:19)

LEAQ

type.int(SB),

AX

0x01ad

00429

(slice.go:19)

PCDATA

$2,

$0

0x01ad

00429

(slice.go:19)

MOVQ

AX,

(SP)

0x01b1

00433

(slice.go:19)

XORPS

X0,

X0

0x01b4

00436

(slice.go:19)

MOVUPS

X0,

16

(SP)

0x01b9

00441

(slice.go:19)

MOVQ

$1,

32

(SP)

0x01c2

00450

(slice.go:19)

CALL

runtime.growslice(SB)

0x01c7

00455

(slice.go:19)

PCDATA

$2,

$1

0x01c7

00455

(slice.go:19)

MOVQ

40

(SP),

AX

0x01cc

00460

(slice.go:19)

MOVQ

48

(SP),

CX

0x01d1

00465

(slice.go:19)

MOVQ

56

(SP),

DX

0x01d6

00470

(slice.go:19)

MOVQ

$1,

(AX)

0x01dd

00477

(slice.go:20)

PCDATA

$2,

$0

0x01dd

00477

(slice.go:20)

MOVQ

AX,

(SP)

0x01e1

00481

(slice.go:19)

LEAQ

1

(CX),

AX

0x01e5

00485

(slice.go:20)

MOVQ

AX,

8

(SP)

0x01ea

00490

(slice.go:20)

MOVQ

DX,

16

(SP)

0x01ef

00495

(slice.go:20)

CALL

runtime.convTslice(SB)

/<code>

上面彙編我們不需要全部看明白只需要看懂6、11、15、23、34、37、44、55、58、61、69、80這幾行就可以,也就是下面的代碼:

<code>slc := 

make

([]

int

,

0

) slc =

append

(slc,

1

) fmt.Println(slc)

0x0021

00033

(slice.

go

:

10

) LEAQ

type

.

int

(SB), AX

0x0034

00052

(slice.

go

:

10

) CALL runtime.makeslice(SB)

0x003e

00062

(slice.

go

:

11

) LEAQ

type

.

int

(SB), CX

0x005f

00095

(slice.

go

:

11

) CALL runtime.growslice(SB)

0x008c

00140

(slice.

go

:

12

) CALL runtime.convTslice(SB) ============================

var

slc1 []

int

slc1 =

append

(slc1,

1

) fmt.Println(slc1)

0x00e8

00232

(slice.

go

:

15

) LEAQ

type

.

int

(SB), AX

0x010d

00269

(slice.

go

:

15

) CALL runtime.growslice(SB)

0x013a

00314

(slice.

go

:

16

) CALL runtime.convTslice(SB)

0x0196

00406

(slice.

go

:

18

) LEAQ

type

.[

0

]

int

(SB), AX ============================ slc2 := []

int

{} slc2 =

append

(slc2,

1

) fmt.Println(slc2)

0x01a1

00417

(slice.

go

:

18

) CALL runtime.newobject(SB)

0x01c2

00450

(slice.

go

:

19

) CALL runtime.growslice(SB)

0x01ef

00495

(slice.

go

:

20

) CALL runtime.convTslice(SB)/<code>

為什麼要看這些彙編代碼呢?go 裡面很多內置的方法我們是不能直接找到對應的實現函數的,所以我們通過這個 go tool compile 工具來看一下,就知道了。上面代碼的第5、6行,就是 make slice 的實現,也就是說調用的函數就是 runtime 包中的 makeslice 函數。接下來的7、8行就是 appen 的具體實現,同樣是 runtime 包,growslice 函數。第9行就是 fmt.Println 的具體實現,也就是 runtime.convTslice 函數。這樣子我們就知道該去哪裡看對應代碼了。其實通過彙編能看到很多東西,這裡我們只說 slice,以後有機會會繼續和大家分享。

上面的彙編代碼我分為了三部分,也就是對應三種 slice 的聲明,估計大家都看出來了,每種聲明方式對應的實現函數都是不一樣的,第一個是 runtime.makeslice,第二個沒有對應的實現,第三個是 runtime.newobject,這三種方式區別還是有的,但是最終都可以實現我們的目標,因為最終都是調用了 runtime.mallocgc 函數,也就是分配內存,看到這有同學就會問了,第二種方式我們並沒有對應的實現啊,也就是說並沒有分配內存啊。是的,第二種方式我們確實沒有直接分配內存,並且我們直接 append 了,其實是因為 append 操作會對沒有分配內存的切片再分配一下內存(感興趣的同學可以仔細看一下 growslice 源碼)。所以我們在寫代碼時,使用 var 關鍵字聲明切片是完全可以的,並且對於長度為0的切片我們非常建議這樣聲明。

我們簡單看一下 makeslice 代碼:

<code>func makeslice(et *_type, 

len

, cap int) unsafe.Pointer { // 判斷 cap 是否溢出 mem, overflow :=

math

.MulUintptr(et.size, uintptr(cap)) // 溢出或者

len

、cap 不符合要求

if

overflow || mem > maxAlloc ||

len

0

||

len

> cap { // 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

) }/<code>

再看一下 newobject 代碼:

<code>

func

newobject

(typ *_type)

unsafe

.

Pointer

{

return

mallocgc(typ.size, typ,

true

) }/<code>

沒錯,上面兩個的最終都是調用 mallocgc 函數。只不過 makeslice 先做了一些判斷。

2.2. 切片源碼

我們看一下切片的源碼(go/src/runtime/slice.go):

<code>

type

slice

struct

{ array unsafe.Pointer

len

int

cap

int

}/<code>

就是這麼簡單,一個切片就是三個字段,第一個是內存開始的指針,第二個是切片的長度,最後一個就是切片的容量。slice.go 文件中其他函數都是針對切片的函數:

  • makeslice:初始化切片。
  • makeslice64:初始化長度和容量為 int64 類型的切片,最終還是調用 makeslice。
  • growslice:切片擴容(內置 append 函數)。
  • slicecopy:切片的拷貝(內置 copy 函數)。

2.3. 切片和數組

切片是數組的片段,也就是說從數組上截取一段,然後我們切片可以在這個數組上“活動”,同樣我們都可以使用下標來訪問某個元素,但是如果我們給切片添加元素時,如果長度超過了當前數組的長度,那我們就再尋找一個新的內存,同樣是新的數組、新的切片,但是對於我們代碼使用者來說,切片還是不變的,但是它在內存的位置改變了。

2.4. append

append 應該是我們最常用的操作了,在文章開始部分我們已經看過簡單的彙編語言了,並且已經知道其源碼就是 runtime/slice.go 中的 growslice 函數。估計大家都知道切片是如何擴容的,網上大多數資料都是當原 slice 容量小於 1024 的時候,新 slice 容量變成原來的 2 倍;原 slice 容量超過 1024,新 slice 容量變成原來的1.25倍。這樣說我們不能說完全正確,這樣的說法還不算嚴謹。為啥不嚴謹呢,我們接著往下看。

append 參數可以是多個,那麼我們就有以下兩種寫法:

<code>s := []

string

{

"a"

,

"b"

} s =

append

(s,

"d"

) s =

append

(s,

"e"

)/<code>

那麼這兩種寫法最後的切片 s 的長度和容量都是多少呢?

<code>s := []

string

{

"a"

,

"b"

} s =

append

(s,

"d"

) s =

append

(s,

"e"

) fmt.Printf(

"len=%d, cap=%d"

,

len

(s),

cap

(s)) /<code>
<code>s := []

string

{

"a"

,

"b"

} fmt.Printf(

"len=%d, cap=%d"

,

len

(s),

cap

(s)) /<code>

事實證明,不同的寫法對切片最終的容量是有影響的。也就是說,對於 append 多個參數時,並不會對每一個元素添加是都會進行擴容,而是對整體的所有元素來進行擴容,並且在元素類型不同時,最終的容量也是不同的。

<code>s := []

int

{

1

,

2

} fmt.Printf(

"len=%d, cap=%d"

,

len

(s),

cap

(s)) /<code>

其原因是元素類型所佔的內存大小是不一樣的,從而導致 append 操作時進行 內存對齊 的結果也不一樣(內存對齊這裡不再贅述,以後有時間也會寫類似文章)。所以我們上面三種代碼最終的結果都是不同的。

結論:在 appen 單個元素時,擴容規律確實是2倍或者1.25倍,但是appen 多個元素時,結果和元素類型是相關的,容量最小和長度相同。

2.5. 切片截取

我們還要注意的是,擴容時如果容量大於原有數據的長度,我們重新分配內存,其操作不會影響原有的數據。但是沒有分配新的內存,也就是說還是原來數組的基礎上添加元素,那麼新的切片操作就會影響原有的數組。這部分依然不再贅述,看一下下面代碼,大家都明白了:

<code>

s

:=

[]int{1,

2

,

3

,

4

}

s1

:=

s[1:2]

fmt.Printf("len=%d,

cap=%d\n",

len(s),

cap(s))

//

len=4,

cap=4fmt.Println(s)

//

[1

2

3

4

]fmt.Println(s1)

//

[2]fmt.Printf("len=%d,

cap=%d\n",

len(s1),

cap(s1))

//

len=1,

cap=3

s1[0]

=

5

fmt.Println(s)

//

[1

5

3

4

]fmt.Println(s1)

//

[5]

s1

=

append(s1,

1

)

fmt.Printf("len=%d,

cap=%d\n",

len(s1),

cap(s1))

//

len=2,

cap=3s1[0]

=

6

fmt.Println(s)

//

[1

6

1

4

]fmt.Println(s1)

//

[6

1

]

s1

=

append(s1,

1

,

2

,

3

)

fmt.Printf("len=%d,

cap=%d\n",

len(s1),

cap(s1))

//

len=5,

cap=6s1[0]

=

7

fmt.Println(s)

//

[1

6

1

4

]fmt.Println(s1)

//

[7

1

1

2

3

]

/<code>

另外還有一點,切片截取也是可以截取 cap 的:

<code>s := []

int

{

1

,

2

,

3

,

4

} fmt.Printf(

"len=%d, cap=%d\n"

,

len

(s),

cap

(s)) s1 := s[

1

:

2

:

3

] fmt.Printf(

"len=%d, cap=%d\n"

,

len

(s1),

cap

(s1)) s2 := s[

1

:

2

] fmt.Printf(

"len=%d, cap=%d\n"

,

len

(s2),

cap

(s2)) /<code>

2.6. 拷貝

切片的拷貝可以使用內置 copy 函數:

<code>

s

:=

[]int{1,

2

,

3

,

4

}

var

s1

[]int

copy(s1,

s)

fmt.Println(s1)

//

[]fmt.Println(s)

//

[1

2

3

4

]

s1

=

make([]int,

2

)

count

:=

copy(s1,

s)

fmt.Println(s1)

//

[1

2

]fmt.Println(s)

//

[1

2

3

4

]fmt.Println(count)

//

2

s1[0]

=

5

fmt.Println(s1)

//

[5

2

]fmt.Println(s)

//

[1

2

3

4

]

/<code>

上面的例子可以看出來,copy 只會拷貝目標切片長度個元素,並且 copy 後兩個切片是沒有影響的。

還有一種更加高效的實現方式:

<code>

s2

:=

append(s[:0:0],

s...)

fmt.Println(s2)

//

[1

2

3

4

]s2[0]

=

5

fmt.Println(s2)

//

[5

2

3

4

]fmt.Println(s)

//

[1

2

3

4

]

/<code>

上面這種方式算是一個比較取巧的方法,並且可以達到 copy 的效果,並且速度要比 copy 快很多。需要注意的是第一行中 cap 的下標是一定要寫的,並且建議寫0,這樣效率是最高的。

2.7. 傳遞

Go 語言函數參數都是值傳遞,在函數內部會拷貝一份,所以 slice 傳遞後拷貝一份都是新的 slice,但是 map 這種初始化之後就是一個 *hmap,所以參數傳遞後還是指向同一個 map(map 以後再詳解)。

2.8. 總結

切片暫時告一段落,最後總結一下:

  • 切片是對數組的封裝,切片可以自動擴容。
  • 切片添加後,新的容量小於之前的容量,那麼還是使用原有的數組,並且兩者之間的元素修改有影響,因為底層是同一塊內存。
  • 切片的擴容並不一定總是原有容量的2倍或者1.25倍,當 append 多個元素時,會有內存對齊 ,最終的容量大於等於長度(這是一句廢話,容量小於長度就 panic 了)。
  • 使用 var 聲明的切片可以直接 append,並且我們建議這樣,因為在聲明時不會分配內存。
  • 已知切片容量或者長度時,聲明時最好也指定容量或者長度,因為擴容導致重新分配內存消耗太大了。

參考鏈接:

  • https://qcrao.github.io/2019/04/02/dive-into-go-slice/(饒大)
  • https://xargin.com/go-slice/(曹大)
  • https://www.tutorialspoint.com/go/go_slice.htm(需要梯子)
  • https://medium.com/rungo/the-anatomy-of-slices-in-go-6450e3bb2b94(需要梯子)
  • https://www.thegeekstuff.com/2019/03/golang-slice-examples/(需要梯子)


分享到:


相關文章: