本文約3439字,建議收藏後閱讀。
我的個人博客:www.godeamon.com。博客看代碼更加方便。
1.切片是啥
Go 的 Slice(切片)類型提供了一種方便有效的方法來處理類型化數據序列。slice 與其他語言的數組類似卻又不同,簡單來說,切片更加靈活,用起來更方便,其原因就是可以擴容。
2.舉例分析
2.1. 認識切片第一步
<code>package
mainimport
"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
) LEAQtype
.int
(SB), AX0x0034
00052
(slice.go
:10
) CALL runtime.makeslice(SB)0x003e
00062
(slice.go
:11
) LEAQtype
.int
(SB), CX0x005f
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
) LEAQtype
.int
(SB), AX0x010d
00269
(slice.go
:15
) CALL runtime.growslice(SB)0x013a
00314
(slice.go
:16
) CALL runtime.convTslice(SB)0x0196
00406
(slice.go
:18
) LEAQtype
.[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'
istrue
too, but since the cap is only being // supplied implicitly, sayinglen
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
slicestruct
{ array unsafe.Pointerlen
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/(需要梯子)