Go語言切片深層解析

一、Go語言中切片類型出現的原因

切片是一種數據類型,這種數據類型便於使用和管理數據集合。

創建一個100萬個int類型元素的數組,並將它傳遞給函數,將會發生什麼?

var array [le6]int

foo(array)

fun foo(array [le6]int){

...

}

在64位架構上,100個int類型的數組需要800萬字節,即8M的內存。由於Go語言只有值傳遞,每次調用函數都需要在棧上分配8M的空間並將數組內容複製進去,這不僅浪費內存而且複製還消耗CPU,當數組較大時複製速度較慢也影響程序使用體驗。因此可以只需要傳入數組的地址,地址在64為系統上只需要消耗8字節,這樣可以更好的利用內存和提升性能,但是由於傳入的指針,當函數內部修改了指針的指向內容數組也會發生改變,因此設計了切片來處理數這類數組的共享問題。

二、切片深層解析

面試題

func main() {

s := []int{1, 2, 3}

ss := s[1:]

ss = append(ss, 4)

for _, v := range ss {

v += 10

}

for i := range ss {

ss[i] += 10

}

fmt.Println(s)

}

上面那道面試題是對於切片的考察,首先我們需要明白切片的結構。

slice在Go的運行時庫中就是一個C語言動態數組的實現,在$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

};

這個結構有3個字段,第一個字段表示array的指針,就是真實數據的指針(這個一定要注意),第二個是表示slice的長度,第三個是表示slice的容量,特別需要注意的是:

slice的長度和容量都不是指針

Go語言切片深層解析

但我們使用 make([]byte, 5) 創建一個切片變量 s 時,它內部的存儲的結構如下:

Go語言切片深層解析

長度是切片引用的元素數目,容量是底層數組的元素數目(從切片指針開始)。

我們對 s 進行切片,觀察切片的數據結構和它引用的底層數組:

s=s[2:4]

Go語言切片深層解析

切片操作並不複製切片指向的元素。它創建一個新的切片並複用原來切片的底層數組。 這使得切片操作和數組索引一樣高效。因此,通過一個新切片修改元素會影響到原始切片的對應元素。

前面創建的切片 s 長度小於它的容量。我們可以增長切片的長度為它的容量:

s = s[:cap(s)]

Go語言切片深層解析

切片增長不能超出其容量。增長超出切片容量將會導致運行時異常,就像切片或數組的索引超 出範圍引起異常一樣。同樣,不能使用小於零的索引去訪問切片之前的元素。

三、切片的創建與使用

剛開始使用切片類型的時候很多人很疑惑這樣一個問題:

fun main(){

slice :=[]int{1,2,3}

changeSlice(slice)

fmt.Println("slice:",slice)

}

func changeSlice(s []int){

s=append(s,10)

}

這個問題的輸出是: 1 2 3

為什麼10沒有append到切片裡面了?

因為通過函數傳遞slice作為參數的時候,形參拷貝實參的slice結構,但是由於 array部分是指針因此形參與實參共享底層數組,但是len和cap是會發生拷貝,當形參s進行append的時候,len會發生變化,但是實參的len沒變,當輸出實參slice的值時,只根據它現在的len進行輸出,因此輸出1 2 3。同理:

slice:=[]int{1,2,3}

s=slice[0:2]

s.append(s,10)

雖然slice與s同用底層數組,但是slice與s的len不相同,因此輸出的slice值與s值也不相同。

創建和初始化切片

1、通過數組創建初始化slice

str :=[5]string{"red","blue","Green","Yellow","Pink"}

slice :=str[:]

使用數組初始化創建切片後,切片會與切片共享底層數組,當修改切片或者數組的值時會相互影響,直到如果對切片添加數據超出cap限制,則會為新切片對象重新分配數組。

2、通過make創建並初始化切片

通過make創建切片需要指定至少出入一個參數,指定切片的長度,如果只指定切片的長度,那麼切片的容量與長度相等。也可以分別指定長度與容量,且容量要大於等於長度。

通過make創建的切片會自動初始化slice長度範圍內值為0。

面試題

func main() {

s := make([]int, 5)

s = append(s, 1, 2, 3)

fmt.Println(s)

}

結果為: 0 0 0 0 0 1 2 3

3、通過切片字面量創建切片

str :=[]string{"red","blue","Green","Yellow","Pink"}

切片的長度與容量會基於初始化提供的元素的個數確定。

使用切片字面量時,可以設置長度和容量,slice:=[]string{99:""},創建長度與容量都是100個元素的切片。

如果在[]運算符裡面指定一個值,那麼創建是數組而不是切片。

nil與空切片

var slice []int

b:=[]int{}

println(a == nil,b==nil)

結果 true false

前者僅僅定義了一個[]int類型的變量,並未執行初始化操作,而後者初始化表達式完成了全部的創建。

但需要描述一個不存在的切片的時候nil很好使用,常用在函數返回。

空切片在底層數組包含0個元素,沒有分配任何空間。表示空集合的時候空切片很好使用。

切片的增長

相對於數組而言,實用切片的一個好處就是可以按需增加切片的容量。Go語言內置的append函數會處理增加長度時所有的操作細節。

使用append時,需要一個被操作的切片和一個要追加的值。函數append調用返回時,會返回一個包含修改結果的新切片。函數append總會增加新切片的長度,而容量有可能會發生改變,也可能不會改變,這取決於被操作切片的可用容量。

如果切片底層數組沒有足夠的可用容量,append函數會創建一個新的底層數組,將被引用的現有值複製到新數組裡,再追加新的值。

slice:=[]int{1,2,3,4}

newSlice :=append(slice,50)

append後,newSlice和slice使用不同的底層數組。

函數append會智能地處理底層數組的容量增長。在切片的容量小於1000個元素時,總是會成倍的增加容量。一旦元素個數超過1000,容量的增長因子會設為1.25。隨著增長算法的改變,增長因子有可能會發生改變。

四、可能的“陷阱”

切片操作並不會複製底層的數組。整個數組將被保存在內存中,直到它不再被引用。 有時候可能會因為一個小的內存引用導致保存所有的數據。

例如, FindDigits 函數加載整個文件到內存,然後搜索第一個連續的數字,最後結果以切片方式返回

var digitRegexp = regexp.MustCompile("[0-9]+")

func FindDigits(filename string) []byte {

b, _ := ioutil.ReadFile(filename)

return digitRegexp.Find(b)

}

這段代碼的行為和描述類似,返回的 []byte 指向保存整個文件的數組。因為切片引用了原始的數組, 導致 GC 不能釋放數組的空間;只用到少數幾個字節卻導致整個文件的內容都一直保存在內存裡。

要修復整個問題,可以將感興趣的數據複製到一個新的切片中:

func CopyDigits(filename string) []byte {

b, _ := ioutil.ReadFile(filename)

b = digitRegexp.Find(b)

c := make([]byte, len(b))

copy(c, b)

return c

}


分享到:


相關文章: