一、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的長度和容量都不是指針
但我們使用 make([]byte, 5) 創建一個切片變量 s 時,它內部的存儲的結構如下:
長度是切片引用的元素數目,容量是底層數組的元素數目(從切片指針開始)。
我們對 s 進行切片,觀察切片的數據結構和它引用的底層數組:
s=s[2:4]
切片操作並不複製切片指向的元素。它創建一個新的切片並複用原來切片的底層數組。 這使得切片操作和數組索引一樣高效。因此,通過一個新切片修改元素會影響到原始切片的對應元素。
前面創建的切片 s 長度小於它的容量。我們可以增長切片的長度為它的容量:
s = s[:cap(s)]
切片增長不能超出其容量。增長超出切片容量將會導致運行時異常,就像切片或數組的索引超 出範圍引起異常一樣。同樣,不能使用小於零的索引去訪問切片之前的元素。
三、切片的創建與使用
剛開始使用切片類型的時候很多人很疑惑這樣一個問題:
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
}