Go語(yǔ)言 slice介紹

2018-07-25 17:22 更新

一個(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à),以避免傳遞顯式索引。

slice的擴(kuò)容

其實(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ī)則是:

  • 如果新的大小是當(dāng)前大小2倍以上,則大小增長(zhǎng)為新大小
  • 否則循環(huán)以下操作:如果當(dāng)前大小小于1024,按每次2倍增長(zhǎng),否則每次按當(dāng)前大小1/4增長(zhǎng)。直到增長(zhǎng)的大小超過(guò)或等于新大小。

make和new

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ì)。

slice與unsafe.Pointer相互轉(zhuǎn)換

有時(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)

links


以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)