
一、定義
我們都知道在 Go 語言中,數(shù)組的長度是不可變的,那么為了更加靈活的處理數(shù)據(jù),Go 提供了一種功能強悍的類型切片(slice),slice 可以理解為 “動態(tài)數(shù)組”。但是 slice 并不是真正意義上的動態(tài)數(shù)組,而是一個引用類型。slice 總是指向一個底層 array,slice 的聲明也可以像 array 一樣,只是不需要長度。slice 的聲明和數(shù)組類似,如下
var iSlice []int
這里的聲明和數(shù)組一樣,只是少了長度,注意兩者的比較
//聲明一個保存 int 的 slice
var iSlice []int
//聲明一個長度為 10 的 int 數(shù)組
var iArray [10]int
還有一種聲明的方法是使用 make() 函數(shù),如下
slice1 := make([]int, 5, 10)
用 make() 函數(shù)創(chuàng)建的時候有三個參數(shù),make(type, len[, cap]) ,依次是類型、長度、容量。

如圖所示,上圖表示創(chuàng)建了 slice1 ,長度是 5,默認的值都是 0,容量是 10,這樣聲明就開辟了一塊容量是 10
的連續(xù)的一塊內(nèi)存。當然如果我們不指定容量也是可以的,如下
slice2 := make([]int, 5)
這樣就會根據(jù)實際情況動態(tài)分配內(nèi)存,而不是最開始指定一塊固定大小的內(nèi)存。需要注意的是我們一般使用 make() 函數(shù)來創(chuàng)建 slice,因為我們可以指定 slice 的容量,這樣在最開始創(chuàng)建的時候就分配好空間,避免數(shù)據(jù)多次改變導(dǎo)致多次重新改變 cap 分配空間帶來不必要的開銷。
二、slice 的特性
關(guān)于 slice 的一些基本特性,《Go Web 編程》 這本書里已經(jīng)講的很詳細,有對基本知識不清楚的童鞋可以去補習一下,這里就不一一敘述了。我么來看一個例子,
package main
import (
"fmt"
)
func main() {
aSlice := []int{1, 2, 3, 4, 5}
fmt.Printf("aSlice length = %d, cap = %d, self = %v\n", len(aSlice), cap(aSlice), aSlice)
aSlice = append(aSlice, 6)
fmt.Printf("aSlice length= %d, cap = %d, self = %v", len(aSlice), cap(aSlice), aSlice)
}
這個時候我們運行,控制臺打印

我們會看到 aSlice 進行 append 操作以后,它的容量增加了一倍,cap 并沒有變成我們想象中的 6 ,而是變成了 10

如果我們最開始 slice 的容量是 10,長度是 5 ,那么再加一個元素是不會改變切片的容量的。也就是說,當我們往 slice中增加元素超過原來的容量時,slice 會自增容量,當現(xiàn)有長度 < 1024 時 cap 增長是翻倍的,當超過 1024,cap 的增長是 1.25 倍增長。我們來看一下 slice.go 的源碼會發(fā)現(xiàn)有這樣一個函數(shù),里面說明了 cap 的增長規(guī)則
func growslice(et *_type, old slice, cap int) slice {
/**
....省略....
**/
newcap := old.cap
doublecap := newcap + newcap
if cap > doublecap {
newcap = cap
} else {
if old.len < 1024 {
newcap = doublecap
} else {
// Check 0 < newcap to detect overflow
// and prevent an infinite loop.
for 0 < newcap && newcap < cap {
newcap += newcap / 4
}
// Set newcap to the requested cap when
// the newcap calculation overflowed.
if newcap <= 0 {
newcap = cap
}
}
}
/**
....省略....
**/
}
從上面的源碼,在對 slice 進行 append 等操作時,可能會造成 slice 的自動擴容。其擴容時的大小增長規(guī)則是:
- 如果新的 slice 大小是當前大小2倍以上,則大小增長為新大小
- 否則循環(huán)以下操作:如果當前slice大小小于1024,按每次 2 倍增長,否則每次按當前大小 1/4 增長,直到增長的大小超過或等于新大小。
- append 的實現(xiàn)只是簡單的在內(nèi)存中將舊 slice 復(fù)制給新 slice
來看一個例子,
package main
import "fmt"
func main() {
aSlice := make([]int, 3, 5)
bSlice := append(aSlice, 1, 2)
fmt.Printf("a %v , cap = %d, len = %d\n", aSlice, cap(aSlice), len(aSlice))
fmt.Printf("b %v , cap = %d, len = %d\n", bSlice, cap(bSlice), len(bSlice))
aSlice[0] = 6
fmt.Printf("a %v , cap = %d, len = %d\n", aSlice, cap(aSlice), len(aSlice))
fmt.Printf("b %v , cap = %d, len = %d", bSlice, cap(bSlice), len(bSlice))
}
我們會看到控制臺輸出

變化過程如下圖所示

上面說明,在 slice 的 cap 范圍內(nèi)增加元素, slice 只會發(fā)生 len 的變化不會發(fā)生 cap 的變化,同樣也說明 slice 實際上是指向一個底層的數(shù)組,當多個 slice 指向同一個底層數(shù)組的時候,其中一個改變,其余的也會跟著改變,這里需要注意一下。我們同樣從 slice.go 的源碼中 slice 的定義可以看出,
type slice struct {
array unsafe.Pointer
len int
cap int
}
這里關(guān)于底層的東西就不多敘述,有興趣的可以看看 一縷殤流化隱半邊冰霜 冰霜的 深入解析 Go 中 Slice 底層實現(xiàn) 這篇文章,對 slice 的底層實現(xiàn)的講解。接下來我們把上面的代碼改變一下
package main
import "fmt"
func main() {
aSlice := make([]int, 3, 5)
bSlice := append(aSlice, 1, 2, 3, 4)
fmt.Printf("a %v , cap = %d, len = %d\n", aSlice, cap(aSlice), len(aSlice))
fmt.Printf("b %v , cap = %d, len = %d\n", bSlice, cap(bSlice), len(bSlice))
aSlice[0] = 6
fmt.Printf("a %v , cap = %d, len = %d\n", aSlice, cap(aSlice), len(aSlice))
fmt.Printf("b %v , cap = %d, len = %d", bSlice, cap(bSlice), len(bSlice))
}
我們可以看到下面的輸出

上面代碼可以用下圖說明

也就是說,當 append 的數(shù)據(jù)超過原來的容量以后,就會重新分配一塊新的內(nèi)存,并把原來的數(shù)據(jù) copy 過來,并且保留原來的空間,供原來的 slice(aSlice) 使用這樣 aSlice 和 bSlice 就各自指向不同的地址,當 aSlice 改變時,bSlice 不會改變。
關(guān)于 cap 還有一點需要注意,我們來用一個例子說明
package main
import "fmt"
func main() {
Array_a := [10]byte{'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'}
Slice_a := Array_a[2:5]
Slice_b := Slice_a[6:7]
fmt.Printf("Slice_a %v , cap = %d, len = %d\n", string(Slice_a), cap(Slice_a), len(Slice_a))
fmt.Printf("Slice_b %v , cap = %d, len = %d\n", string(Slice_b), cap(Slice_b), len(Slice_b))
}
控制臺打印

這里我們會發(fā)現(xiàn) Slice_b 對 Slice_a 進行重新切片后,并沒有報錯,而是還有輸出,這是因為 Slice_a 的 cap 是 8 ,并不是我們想象的 3,slice 指向的是一塊連續(xù)的內(nèi)存,所以 Slice_a 的容量其實是一直到 Array_a 的最后的。所以這里 Array_b 對 Array_a 進行切片后會得到值,《Go Web 編程》 上這張圖形象的解釋了對數(shù)組的切片結(jié)果,這里是需要注意的一個點。

三、關(guān)于 copy
我們來看下面代碼
package main
import "fmt"
func main() {
aSlice := []int{1, 2, 3}
bSlice := []int{4, 5, 6, 7, 8, 9}
copy(bSlice, aSlice)
fmt.Println(aSlice, bSlice)//[1 2 3] [1 2 3 7 8 9]
//如果是 copy( aSlice, bSlice) 則結(jié)果是 [4 5 6]
}
也就是說 copy() 函數(shù)有兩個參數(shù),一個是 to 一個是 from,就是將第二個 copy 到第一個上面,如果第一個長度小于第二個,那么就會 copy 與第一個等長度的值,如 copy( aSlice, bSlice) 的結(jié)果是 [4 5 6] ,反之則是短的覆蓋長的前幾位。當然我們也可以指定復(fù)制長度
package main
import "fmt"
func main() {
aSlice := []int{1, 2, 3}
bSlice := []int{4, 5, 6, 7, 8, 9}
copy(bSlice[2:5], aSlice)
fmt.Println(aSlice, bSlice)//[1 2 3] [4 5 1 2 3 9]
}
關(guān)于 slice 的 copy 的規(guī)則邏輯我們也可以在源碼中看出
func slicecopy(to, fm slice, width uintptr) int {
if fm.len == 0 || to.len == 0 {
return 0
}
n := fm.len
if to.len < n {
n = to.len
}
if width == 0 {
return n
}
if raceenabled {
callerpc := getcallerpc()
pc := funcPC(slicecopy)
racewriterangepc(to.array, uintptr(n*int(width)), callerpc, pc)
racereadrangepc(fm.array, uintptr(n*int(width)), callerpc, pc)
}
if msanenabled {
msanwrite(to.array, uintptr(n*int(width)))
msanread(fm.array, uintptr(n*int(width)))
}
size := uintptr(n) * width
if size == 1 { // common case worth about 2x to do here
// TODO: is this still worth it with new memmove impl?
*(*byte)(to.array) = *(*byte)(fm.array) // known to be a byte pointer
} else {
memmove(to.array, fm.array, size)
}
return n
}
我們看源碼接著往下看會發(fā)現(xiàn)這樣一個方法
func slicestringcopy(to []byte, fm string) int {
if len(fm) == 0 || len(to) == 0 {
return 0
}
n := len(fm)
if len(to) < n {
n = len(to)
}
if raceenabled {
callerpc := getcallerpc()
pc := funcPC(slicestringcopy)
racewriterangepc(unsafe.Pointer(&to[0]), uintptr(n), callerpc, pc)
}
if msanenabled {
msanwrite(unsafe.Pointer(&to[0]), uintptr(n))
}
memmove(unsafe.Pointer(&to[0]), stringStructOf(&fm).str, uintptr(n))
return n
}
我們會發(fā)現(xiàn)這個函數(shù)的兩個參數(shù)分別是 []byte 和 string ,這里其實是 Go 實現(xiàn)了一個將 string 復(fù)制到 []byte 上的方法,這個方法有什么用,我們來看個例子
package main
import "fmt"
func main() {
s := "hello"
c := []byte(s) // 將字符串 s 轉(zhuǎn)換為 []byte 類型
c[0] = 'c'
s2 := string(c) // 再轉(zhuǎn)換回 string 類型
fmt.Printf("%s\n", s2)
fmt.Printf("s-%x, c-%x, s2-%x", &s, &c, &s2)
}
控制臺輸出

在 Go 中字符串是不可以改變的,我們可以用上面的方法來改變字符串,這里可以看到是實現(xiàn)了 string 和 []byte 的互相轉(zhuǎn)換,達到了修改 string 的目的。我們?nèi)タ纯?string.go 的源碼會發(fā)現(xiàn),有下面的方法
func stringtoslicebyte(buf *tmpBuf, s string) []byte {
var b []byte
if buf != nil && len(s) <= len(buf) {
*buf = tmpBuf{}
b = buf[:len(s)]
} else {
b = rawbyteslice(len(s))
}
copy(b, s)
return b
}
可以看到上面有個 copy(b, s) ,這里就是將 string 復(fù)制到 []byte 上,在 slice.go 已經(jīng)實現(xiàn)過了的。從源碼中我們也可以看出每次 b 都是重新分配的,然后將 s 復(fù)制 給 b,從我們上面程控制臺輸出也可以看到每次地址都有變化,所以說 string 和 []byte 的相互轉(zhuǎn)換是有內(nèi)存開銷的,不過對于現(xiàn)在的機器來說,這點開銷也不算什么。
最后,這是我學(xué)習 Go 的 slice 的一些理解與總結(jié),由于能力有限,如果有理解不到位的地方,可以隨時留言與我交流。
參考:
1、build-web-application-with-golang
2、go