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的長度和容量都不是指針

但我們使用 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
}
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 線性結構是計算機最常用的數據結構之一。無論是數組(arrary)還是鏈表(list),在編程中不可或缺。golan...
    _二少爺閱讀 6,642評論 5 13
  • 出處---Go編程語言 歡迎來到 Go 編程語言指南。本指南涵蓋了該語言的大部分重要特性 Go 語言的交互式簡介,...
    Tuberose閱讀 18,492評論 1 46
  • 元正同學今天破天荒的第一次起了個大早,六點鐘不到的樣子就在被窩里嘚啵了。“爸爸呢?他去見朋友啦?”“他去接姐...
    元正媽媽閱讀 211評論 0 0
  • 認真二字是提命于自己的 無論何時何境,認真活著 一壺水一碗粥 一彎腰一席凈地 痛與癢是偷縫的陰霾,估計最怕認真二字
    異常值閱讀 137評論 0 0
  • 苦蟬孑立老樹喧啾, 歌訣里揶揄著離愁 灼洗大地的驕陽不語, 楊柳滌去滄桑,舒展枝頭 頑童仍舊似風競逐 控訴溪水掠奪...
    Mr遠遠閱讀 201評論 0 0