1. 程式人生 > >細說Go語言切片

細說Go語言切片

  目錄
  
  內部實現
  
  宣告切片
  
  初始化陣列
  
  切片記憶體結構
  
  使用切片
  
  迭代切片
  
  切片重組
  
  在函式間傳遞切片
  
  new()和make()的區別
  
  字串、陣列和切片的應用
  
  從字串生成位元組切片
  
  字串和切片的記憶體結構
  
  修改字串中的某個字元
  
  在上一篇文章中已經瞭解了陣列,陣列有特定的用處,但是卻有一些呆板(陣列長度固定不可變),所以在 Go 語言的程式碼裡並不是特別常見。接下來聊聊切片(slice),相對的,切片卻是隨處可見的,Go語言切片是一種建立在陣列型別之上的抽象,它構建在陣列之上並且提供更強大的能力和便捷。
  
  內部實現
  
  切片(slice)是對陣列一個連續片段的引用(該陣列我們稱之為相關陣列,通常是匿名的),所以切片是一個引用型別(因此更類似於 C/C++ 中的陣列型別)。這個片段可以是整個陣列,或者是由起始和終止索引標識的一些項的子集。需要注意的是,終止索引標識的項不包括在切片內。切片提供了一個相關陣列的動態視窗。
  
  切片是可索引的,並且可以由 len() 函式獲取長度。
  
  給定項的切片索引可能比相關陣列的相同元素的索引小。和陣列不同的是,切片的長度可以在執行時修
  
  改,最小為 0 最大為相關陣列的長度:切片是一個 長度可變的陣列。
  
  切片物件非常小,是因為它是隻有3個欄位的資料結構:一個是指向底層陣列的指標,一個是切片的長度,一個是切片的容量。這3個欄位,就是Go語言操作底層陣列的元資料,有了它們,我們就可以任意的操作切片了。
  
  切片提供了計算容量的函式 cap() 可以測量切片最長可以達到多少:它等於切片的長度 + 陣列除切片之外的長度。如果 s 是一個切片, cap(s) 就是從 s[0] 到陣列末尾的陣列長度。切片的長度永遠不會超過它的容量,所以對於 切片 s 來說該不等式永遠成立: 0 <= len(s) <= cap(s) 。
  
  多個切片如果表示同一個陣列的片段,它們可以共享資料;因此一個切片和相關陣列的其他切片是共享儲存的,相反,不同的陣列總是代表不同的儲存。陣列實際上是切片的構建塊。
  
  優點 因為切片是引用,所以它們不需要使用額外的記憶體並且比使用陣列更有效率,所以在 Go 程式碼中 切片比陣列更常用。
  
  宣告切片
  
  宣告切片的方式和宣告陣列的方式差不都
  
  //   變數名            變數型別
  
  var variable_name = []var_type
  
  如果切片只宣告而沒有初始化,那麼這個切片的預設值為nil,長度為 0。
  
  package main
  
  import "fmt"
  
  func main() {
  
  var sli []int
  
  if sli == nil {
  
  fmt.Println("sli和nil相等")
  
  } else {
  
  fmt.Println("sli和nil不相等")
  
  }
  
  }
  
  初始化陣列
  
  make方式建立切片,對應用型別的資料都可以使用make函式建立,該函式會返回該型別的引用。
  
  slice := make([]int, 10, 20)
  
  這裡我們建立了一個型別為[]int,長度為10,容量為20的切片,如果不指定切片的容量,例如slice := make([]int, 10),那麼該切片的容量和長度相等。
  
  因為切片的底層是陣列,所以建立切片時,如果不指定字面值的話,預設值就是陣列的元素的零值。這裡我們所以指定了容量是20,但是我們職能訪問10個元素,因為切片的長度是10,剩下的10個元素,需要切片擴充後才可以訪問。
  
  容量必須>=長度,我們是不能建立長度大於容量的切片的。
  
  還有一種建立切片的方式,是使用字面量,就是指定初始化的值。
  
  slice := []int{1,2,3,4,5}
  
  通過字面量建立切片和建立陣列的方式非常像,只不過不用指定[]中的值([]裡面沒有...),這時候切片的長度和容量是相等的,並且會根據我們指定的字面量推匯出來。當然我們也可以像陣列一樣,只初始化某個索引的值:
  
  slice := []int{4:1}
  
  這是指定了第5個元素為1,其他元素都是預設值0。這時候切片的長度和容量也是一樣的。這裡再次強調一下切片和陣列的微小差別。
  
  //陣列
  
  array := [...]int{4:1}
  
  //切片
  
  slice := []int{4:1}
  
  切片還有nil切片和空切片,它們的長度和容量都是0,但是它們指向底層陣列的指標不一樣,nil切片意味著指向底層陣列的指標為nil,而空切片對應的指標是個地址。
  
  //nil切片
  
  var slice []int
  
  //空切片
  
  slice:=[]int{}
  
  nil切片表示不存在的切片,而空切片表示一個空集合,它們各有用處。
  
  切片另外一個用處比較多的建立是基於現有的陣列或者切片建立。
  
  slice := []int{1, 2, 3, 4, 5}
  
  slice1 := slice[:]
  
  slice2 := slice[0:]
  
  slice3 := slice[:5]
  
  fmt.Println(slice1)
  
  fmt.Println(slice2)
  
  fmt.Println(slice3)
  
  基於現有的切片或者陣列建立,使用[i:j]這樣的操作符即可,她表示以i索引開始,到j索引結束,擷取原陣列或者切片,建立而成的新切片,新切片的值包含原切片的i索引,但是不包含j索引。
  
  i如果省略,預設是0;j如果省略預設是原陣列或者切片的長度,所以例子中的三個新切片的值是一樣的。這裡注意的是i和j都不能超過原切片或者陣列的索引。
  
  slice := []int{1, 2, 3, 4, 5}
  
  newSlice := slice[1:3]
  
  newSlice[0] = 10
  
  fmt.Println(slice)
  
  fmt.Println(newSlice)
  
  fmt.Printf("%p\n", &slice[1])
  
  fmt.Printf("%p\n", &newSlice[0])
  
  這個例子證明了,新的切片和原切片共用的是一個底層陣列,所以當修改的時候,底層陣列的值就會被改變,所以原切片的值也改變了。當然對於基於陣列的切片也一樣的。
  
  我們基於原陣列或者切片建立一個新的切片後,那麼新的切片的大小和容量是多少呢?這裡有個公式:
  
  對於底層陣列容量是k的切片slice[i:j]來說
  
  長度:j-i
  
  容量:k-i
  
  比如我們上面的例子slice[1:3],長度就是3-1=2,容量是5-1=4。不過程式碼中我們計算的時候不用這麼麻煩,因為Go語言為我們提供了內建的len和cap函式來計算切片的長度和容量。
  
  slice := []int{1, 2, 3, 4, 5}
  
  newSlice := slice[1:3]
  
  fmt.Printf("newSlice長度:%d,容量:%d",len(newSlice),cap(newSlice))
  
  以上基於一個數組或者切片使用2個索引建立新切片的方法,此外還有一種3個索引的方法,第3個用來限定新切片的容量,其用法為slice[i:j:k]。
  
  slice := []int{1, 2, 3, 4, 5}
  
  newSlice := slice[1:2:3]
  
  這樣我們就建立了一個長度為2-1=1,容量為3-1=2的新切片,不過第三個索引,不能超過原切片的最大索引值5。
  
  切片記憶體結構
  
  var slice = []int{1, 2, 3, 4, 5}這樣就建立了一個長度為5的陣列並且建立了一個相關切片。
  
  我們可以分析一下,上圖中的切片在記憶體中結構
  
  切片在記憶體中的組織方式實際上是一個有 3 個域的結構體:指向相關陣列的指標,切片長度以及切片容量。
  
  注意 絕對不要用指標指向 slice。切片本身已經是一個引用型別,所以它本身就是一個指標!!
  
  正因為這樣,所以我們陣列首地址需要取地址符(&),但是列印一個切片的地址的時候就不要加取地址符了。
  
  func main() {
  
  arr := [...]int{1, 2, 3, 4 ,5}
  
  slice := []int{1, 2, 3, 4, 5}
  
  fmt.Printf("arr的首地址為: %p\n", &arr)
  
  fmt.Printf("slice的首地址為: %p\n", slice)
  
  }
  
  使用切片
  
  使用切片,和使用陣列一樣,通過索引就可以獲取切片對應元素的值,同樣也可以修改對應元素的值。
  
  slice := []int{1, 2, 3, 4, 5}
  
  fmt.Println(slice[2]) //獲取值
  
  slice[2] = 10 //修改值
  
  fmt.Println(slice[2]) //輸出10
  
  切片只能訪問到其長度內的元素,訪問超過長度外的元素,會導致執行時異常,與切片容量關聯的元素只能用於切片增長。
  
  我們前面講了,切片算是一個動態陣列,所以它可以按需增長,我們使用內建append函式即可。append函式可以為一個切片追加一個元素,至於如何增加、返回的是原切片還是一個新切片、長度和容量如何改變這些細節,append函式都會幫我們自動處理。
  
  slice := []int{1, 2, 3, 4, 5}
  
  newSlice := slice[1:3]
  
  newSlice=append(newSlice,10)
  
  fmt.Println(www.feifanyule.cn newSlice)
  
  fmt.Println(www.haom178.com slice)
  
  //Output
  
  [2 3 10]
  
  [1 2 3 10 5]
  
  例子中,通過append函式為新建立的切片newSlice,追加了一個元素10,我們發現列印的輸出,原切片slice的第4個值也被改變了,變成了10。引起這種結果的原因是因為newSlice有可用的容量,不會建立新的切片來滿足追加,所以直接在newSlice後追加了一個元素10,因為newSlice和slice切片共用一個底層陣列,所以切片slice的對應的元素值也被改變了。
  
  這裡newSlice新追加的第3個元素,其實對應的是slice的第4個元素,所以這裡的追加其實是把底層陣列的第4個元素修改為10,然後把newSlice長度調整為3。
  
  如果切片的底層陣列,沒有足夠的容量時,就會新建一個底層陣列,把原來陣列的值複製到新底層數組裡,再追加新值,這時候就不會影響原來的底層陣列了。
  
  所以一般我們在建立新切片的時候,最好要讓新切片的長度和容量一樣,這樣我們在追加操作的時候就會生成新的底層陣列,和原有陣列分離,就不會因為共用底層陣列而引起奇怪問題,因為共用陣列的時候修改內容,會影響多個切片。
  
  append函式會智慧的增長底層陣列的容量,目前的演算法是:容量小於1000個時,總是成倍的增長,一旦容量超過1000個,增長因子設為1.25,也就是說每次會增加25%的容量。
  
  內建的append也是一個可變引數的函式,所以我們可以同時追加好幾個值。
  
  newSlice=append(newSlice,10,20,30)
  
  此外,我們還可以通過...操作符,把一個切片追加到另一個切片裡。
  
  slice := []int{1, 2, 3, 4, 5}
  
  newSlice := slice[www.michenggw.com 1:2:3]
  
  newSlice=append(newSlice,slice...)
  
  fmt.Println(newSlice)
  
  fmt.Println(slice)
  
  迭代切片
  
  切片是一個集合,我們可以使用 for range 迴圈來迭代它,列印其中的每個元素以及對應的索引。
  
  slice := []int{1, 2, 3, 4, 5}
  
  for i,v:=range slice{
  
  fmt.Printf("索引:%d,值:%d\n",i,v)
  
  }
  
  如果我們不想要索引,可以使用_來忽略它,這是Go語言的用法,很多不需要的函式等返回值,都可以忽略。
  
  slice := [www.mhylpt.com]int{1, 2, 3, 4, 5}
  
  for _,v:=range slice{
  
  fmt.Printf("值:%d\n",v)
  
  }
  
  這裡需要說明的是range返回的是切片元素的複製,而不是元素的引用,所以這裡我們修改v的值並不會改變slice切片裡的值。
  
  除了for range迴圈外,我們也可以使用傳統的for迴圈,配合內建的len函式進行迭代。
  
  slice := []int{1, 2, 3, 4, 5}
  
  for i := 0; i < len(slice); i++ {
  
  fmt.Printf("值:%d\n", slice[i])
  
  }
  
  切片重組
  
  我們已經知道切片建立的時候通常比相關陣列小,例如:
  
  slice1 := make([www.furggw.com]type, start_length, capacity)
  
  其中 start_length 作為切片初始長度而 capacity 作為相關陣列的長度。
  
  這麼做的好處是我們的切片在達到容量上限後可以擴容。改變切片長度的過程稱之為切片重組 reslicing,做法如下: slice1 = slice1[0:end] ,其中 end 是新的末尾索引(即長度)。
  
  將切片擴充套件 1 位可以這麼做:
  
  slice = slice[0:len(slice)+1]
  
  切片可以反覆擴充套件直到佔據整個相關陣列。
  
  在函式間傳遞切片
  
  其實無論是值型別還是引用型別,函式間的傳遞都是值傳遞,只不過值型別的資料傳遞是傳遞的是變數的值,而引用型別在函式間的傳遞的是變數的地址,然而這個地址其實也是一個值。
  
  package main
  
  import "fmt"
  
  func main(www.gcyl152.com) {
  
  slice := []int{1, 2, 3, 4, 5}
  
  fmt.Printf("%p\n", slice)
  
  modify(slice)
  
  fmt.Println(slice)
  
  }
  
  func modify(slice []int) {
  
  fmt.Printf("%p\n", slice)
  
  slice[1] = 10
  
  }
  
  列印的輸出如下:
  
  0xc0000180c0
  
  0xc0000180c0
  
  [1 10 3 4 5]
  
  仔細看,這兩個切片的地址是一樣的,所以這兩個切片指向同一個記憶體地址。因此我們修改一個索引的值後,發現原切片的值也被修改了,說明它們共用一個底層陣列。
  
  new()和make()的區別
  
  看起來二者沒有什麼區別,都在堆上分配記憶體,但是它們的行為不同,適用於不同的型別。
  
  new(T) 為每個新的型別T分配一片記憶體,初始化為 0 並且返回型別為*T的記憶體地址:這種方法 返回一個指向型別為 T,值為 0 的地址的指標,它適用於值型別如陣列和結構體;它相當於 &T{}。
  
  make(T) 返回一個型別為 T 的初始值,它只適用於3種內建的引用型別:切片、map 和 channel
  
  字串、陣列和切片的應用
  
  從字串生成位元組切片
  
  假設 s 是一個字串(本質上是一個位元組陣列),那麼就可以直接通過 c := []bytes(s) 來獲取一個位元組的切片 c。另外,您還可以通過 copy 函式來達到相同的目的: copy(dst []byte, src string) 。
  
  同樣的,還可以使用 for-range 來獲得每個元素
  
  package main
  
  import "fmt"
  
  func main() {
  
  s := "我愛你中國"
  
  for _, value := range s {
  
  fmt.Printf("%c ", value)
  
  }
  
  }
  
  輸出:
  
  我 愛 你 中 國
  
  我們知道,Unicode 字元會佔用 2 個位元組,有些甚至需要 3 個或者 4 個位元組來進行表示。如果發現錯誤的 UTF8 字元,則該字元會被設定為 U+FFFD 並且索引向前移動一個位元組。和字串轉換一樣,您同樣可以使用 c := []int(s) 語法,這樣切片中的每個 int 都會包含對應的 Unicode 程式碼,因為字串中的每次字元都會對應一個整數。類似的,您也可以將字串轉換為元素型別為 rune 的切片: r := []rune(s) 。
  
  可以通過程式碼 len([]rune(s)) 來獲得字串中字元的數量,但使用 utf8.RuneCountInString(s) 效率會更高一點。
  
  您還可以將一個字串追加到某一個字元陣列的尾部:
  
  import "fmt"
  
  func main() {
  
  var r []byte
  
  //比較適合ascii字串,用漢字的話輸出會亂碼
  
  var s string = "I love you"
  
  r = append(r, s...)
  
  for _, v := range r {
  
  fmt.Printf("%c ", v)
  
  }
  
  }
  
  字串和切片的記憶體結構
  
  在記憶體中,一個字串實際上是一個雙字結構,即一個指向實際資料的指標和記錄字串長度的整數。因為指標對使用者來說是完全不可見,因此我們可以依舊把字串看做是一個值型別,也就是一個字元陣列。
  
  字串 string s = "hello" 和子字串 t = s[2:3] 在記憶體中的結構可以用下圖表示:
  
  修改字串中的某個字元
  
  Go 語言中的字串是不可變的,也就是說 str[index] 這樣的表示式是不可以被放在等號左側的。如果嘗試執行 str[i] = 'D' 會得到錯誤: cannot assign to str[i] 。
  
  因此,您必須先將字串轉換成位元組陣列,然後再通過修改陣列中的元素值來達到修改字串的目的,最後將位元組陣列轉換回字串格式。
  
  例如,將字串 "hello" 轉換為 "cello":
  
  s := "hello"
  
  c := []byte(s)
  
  c[0] = 'c'
  
  s2 := string(c) //s2 == "cello"
  
  所以,您可以通過操作切片來完成對字串的操作。