一個(gè)slice是一個(gè)數(shù)組某個(gè)部分的引用。在內(nèi)存中,它是一個(gè)包含3個(gè)域的結(jié)構(gòu)體:指向slice中第一個(gè)元素的指針,slice的長(zhǎng)度,以及slice的容量。長(zhǎng)度是下標(biāo)操作的上界,如x[i]中i必須小于長(zhǎng)度。容量是分割操作的上界,如x[i:j]中j不能大于容量。
數(shù)組的slice并不會(huì)實(shí)際復(fù)制一份數(shù)據(jù),它只是創(chuàng)建一個(gè)新的數(shù)據(jù)結(jié)構(gòu),包含了另外的一個(gè)指針,一個(gè)長(zhǎng)度和一個(gè)容量數(shù)據(jù)。 如同分割一個(gè)字符串,分割數(shù)組也不涉及復(fù)制操作:它只是新建了一個(gè)結(jié)構(gòu)來(lái)放置一個(gè)不同的指針,長(zhǎng)度和容量。在例子中,對(duì)[]int{2,3,5,7,11}
求值操作會(huì)創(chuàng)建一個(gè)包含五個(gè)值的數(shù)組,并設(shè)置x的屬性來(lái)描述這個(gè)數(shù)組。分割表達(dá)式x[1:3]
并不分配更多的數(shù)據(jù):它只是寫(xiě)了一個(gè)新的slice結(jié)構(gòu)的屬性來(lái)引用相同的存儲(chǔ)數(shù)據(jù)。在例子中,長(zhǎng)度為2--只有y[0]和y[1]是有效的索引,但是容量為4--y[0:4]是一個(gè)有效的分割表達(dá)式。
由于slice是不同于指針的多字長(zhǎng)結(jié)構(gòu),分割操作并不需要分配內(nèi)存,甚至沒(méi)有通常被保存在堆中的slice頭部。這種表示方法使slice操作和在C中傳遞指針、長(zhǎng)度對(duì)一樣廉價(jià)。Go語(yǔ)言最初使用一個(gè)指向以上結(jié)構(gòu)的指針來(lái)表示slice,但是這樣做意味著每個(gè)slice操作都會(huì)分配一塊新的內(nèi)存對(duì)象。即使使用了快速的分配器,還是給垃圾收集器制造了很多沒(méi)有必要的工作。移除間接引用及分配操作可以讓slice足夠廉價(jià),以避免傳遞顯式索引。
其實(shí)slice在Go的運(yùn)行時(shí)庫(kù)中就是一個(gè)C語(yǔ)言動(dòng)態(tài)數(shù)組的實(shí)現(xiàn),在$GOROOT/src/pkg/runtime/runtime.h中可以看到它的定義:
struct Slice
{ // must not move anything
byte* array; // actual data
uintgo len; // number of elements
uintgo cap; // allocated number of elements
};
在對(duì)slice進(jìn)行append等操作時(shí),可能會(huì)造成slice的自動(dòng)擴(kuò)容。其擴(kuò)容時(shí)的大小增長(zhǎng)規(guī)則是:
Go有兩個(gè)數(shù)據(jù)結(jié)構(gòu)創(chuàng)建函數(shù):new和make。兩者的區(qū)別在學(xué)習(xí)Go語(yǔ)言的初期是一個(gè)常見(jiàn)的混淆點(diǎn)?;镜膮^(qū)別是new(T)
返回一個(gè)*T
,返回的這個(gè)指針可以被隱式地消除引用(圖中的黑色箭頭)。而make(T, args)
返回一個(gè)普通的T。通常情況下,T內(nèi)部有一些隱式的指針(圖中的灰色箭頭)。一句話,new返回一個(gè)指向已清零內(nèi)存的指針,而make返回一個(gè)復(fù)雜的結(jié)構(gòu)。
有一種方法可以統(tǒng)一這兩種創(chuàng)建方式,但是可能會(huì)與C/C++的傳統(tǒng)有顯著不同:定義make(*T)
來(lái)返回一個(gè)指向新分配的T的指針,這樣一來(lái),new(Point)得寫(xiě)成make(*Point)。但這樣做實(shí)在是和人們期望的分配函數(shù)太不一樣了,所以Go沒(méi)有采用這種設(shè)計(jì)。
有時(shí)候可能需要使用一些比較tricky的技巧,比如利用make弄一塊內(nèi)存自己管理,或者用cgo之類的方式得到的內(nèi)存,轉(zhuǎn)換為Go類型使用。
從slice中得到一塊內(nèi)存地址是很容易的:
s := make([]byte, 200)
ptr := unsafe.Pointer(&s[0])
從一個(gè)內(nèi)存指針構(gòu)造出Go語(yǔ)言的slice結(jié)構(gòu)相對(duì)麻煩一些,比如其中一種方式:
var ptr unsafe.Pointer
s := ((*[1<<10]byte)(ptr))[:200]
先將ptr
強(qiáng)制類型轉(zhuǎn)換為另一種指針,一個(gè)指向[1<<10]byte
數(shù)組的指針,這里數(shù)組大小其實(shí)是假的。然后用slice操作取出這個(gè)數(shù)組的前200個(gè),于是s
就是一個(gè)200個(gè)元素的slice。
或者這種方式:
var ptr unsafe.Pointer
var s1 = struct {
addr uintptr
len int
cap int
}{ptr, length, length}
s := *(*[]byte)(unsafe.Pointer(&s1))
把slice的底層結(jié)構(gòu)寫(xiě)出來(lái),將addr,len,cap等字段寫(xiě)進(jìn)去,將這個(gè)結(jié)構(gòu)體賦給s。相比上一種寫(xiě)法,這種更好的地方在于cap更加自然,雖然上面寫(xiě)法中實(shí)際上1<<10就是cap。
或者使用reflect.SliceHeader的方式來(lái)構(gòu)造slice,比較推薦這種做法:
var o []byte
sliceHeader := (*reflect.SliceHeader)((unsafe.Pointer(&o)))
sliceHeader.Cap = length
sliceHeader.Len = length
sliceHeader.Data = uintptr(ptr)
更多建議: