Go語言切片詳解


1. 切片底層實現

1.1 切片簡介

  Go語言中的切片是圍繞動態數組的概念構建的,可以按需自動增長和縮小。切片的動態增長是通過內置函數append來實現的,還可以通過對切片再次切片來縮小一個切片的大小。因為切片在內存中是連續的,所以切片還能獲得索引、迭代以及垃圾回收優化的好處。

1.2 切片底層實現

  切片的底層實現包含3個字段:指向底層數組的指針、切片訪問的元素的個數(長度)、切片允許增長到的元素的個數(容量),如下圖所示。切片可以理解為對底層數組進行了抽象,並提供了相關的操作方法。
      

 

2. 切片的基礎操作

2.1 創建和初始化

  可以通過make、切片字面量來創建和初始化切片,也可以利用現有數組或切片直接創建切片(Go語言中的引用類型(slice、map、chan)不能使用new進行初始化)。

  1. 使用make時,需要傳入一個參數指定切片的長度,如果只指定長度,則切片的容量和長度相等。也可以傳入兩個參數分別指定長度和容量。不允許創建容量小於長度的切片。
// make只傳入一個參數指定長度,則容量和長度相等。以下輸出:"len: 5, cap: 5"
s := make([]int, 5)
fmt.Printf("len: %d, cap: %d\n", len(s), cap(s))

// make 傳入長度和容量。以下輸出:"len: 5, cap: 10"
s := make([]int, 5, 10)
fmt.Printf("len: %d, cap: %d\n", len(s), cap(s))

// 不允許創建容量小於長度的切片。下面語句編譯會報錯:"len larger than cap in make([]int)"
s := make([]int, 10, 5)
  1. 通過切片字面量來聲明切片。
// 通過字面量聲明切片,其長度和容量都為5。以下輸出:“len: 5, cap: 5”
s := []int{1, 2, 3, 4, 5}
fmt.Printf("len: %d, cap: %d\n", len(s), cap(s))

// 可以在聲明切片時利用索引來給出所需的長度和容量。
// 通過指定索引為99的元素,來創建一個長度和容量為100的切片
s := []int{99: 0}
  1. 基於現有數組或切片來創建切片的方法為:s := baseStr[low:high:max],low指定開始元素下標,high指定結束元素下標,max指定切片能增長到的元素下標。這三個參數都可以省略,low省略默認從下標0開始,high省略默認為最后一個元素下標,max省略默認是底層數組或切片的容量(這里也要注意max不能小於high)。這種方式下,切片的長度和容量的計算方式為:
len = hith - low
cap = max - low
s1 := baseStr[1:3:10]
fmt.Printf("len: %d, cap: %d\n", len(s1), cap(s1)) // len: 2, cap: 9

s2 := baseStr[1:3]
fmt.Printf("len: %d, cap: %d\n", len(s2), cap(s2)) // len: 2, cap: 9

s3 := baseStr[:3]
fmt.Printf("len: %d, cap: %d\n", len(s3), cap(s3)) // len: 3, cap: 10

ss1 := s1[2:5]
ss2 := s1[3:8]
fmt.Printf("len: %d, cap: %d\n", len(ss1), cap(ss1)) // len: 3, cap: 7
fmt.Printf("len: %d, cap: %d\n", len(ss2), cap(ss2)) // len: 5, cap: 6

  基於同一個數組或切片創建的不同切片都共享同一個底層數組。如果一個切片修改了該底層數組的共享部分,其他切片和原始數組或切片都能感知到。其底層數據結構如下面兩個圖所示:
  共享同一底層數組:
      

  改變互相感知:
      

baseSlice := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
s1 := baseSlice[1:5:10]
s2 := baseSlice[2:7]

// 這里 baseSlice、s1、s2 都共享同一個底層數組
/*    s1 起始指針指向 baseSlice 下標為1的元素,可以訪問到baseSlice下標為4的元素,
   可以通過append增加容量到baseSlice最后一個元素。
	  s1 起始指針指向 baseSlice 下標為1的元素,可以訪問到baseSlice下標為4的元素,
   可以通過append增加容量到baseSlice最后一個元素。
*/

// 下面的例子可以看到,不管修改 baseSlice、s1、s2 中的哪個,這幾個切片能訪問到的數據都會跟着改變

// 修改 baseSlice 下標為3元素
/*
	baseSlice: [1 2 3 999 5 6 7 8 9 10]
	s1: [2 3 999 5]
	s2: [3 999 5 6 7]
*/
baseSlice[3] = 999
fmt.Printf("baseSlice: %v\n", baseSlice)
fmt.Printf("s1: %v\n", s1)
fmt.Printf("s2: %v\n", s2)

// 修改 s1 下標為1元素
/*
	baseSlice: [1 2 888 999 5 6 7 8 9 10]
	s1: [2 888 999 5]
	s2: [888 999 5 6 7]
*/
s1[1] = 888
fmt.Printf("baseSlice: %v\n", baseSlice)
fmt.Printf("s1: %v\n", s1)
fmt.Printf("s2: %v\n", s2)

// 修改 s2 下標為2元素
/*
	baseSlice: [1 2 888 999 222 6 7 8 9 10]
	s1: [2 888 999 222]
	s2: [888 999 222 6 7]
*/
s2[2] = 222
fmt.Printf("baseSlice: %v\n", baseSlice)
fmt.Printf("s1: %v\n", s1)
fmt.Printf("s2: %v\n", s2)

2.2 nil和空切片

  用var s []int聲明的切片如果未經初始化,就是nil切片。空切片是用make或字面量創建的切片,s := make([]int, 0)或者s := []int{}。空切片在底層數組包含0個元素,也沒有分配任何存儲空間。不管是空切片還是nil切片,對其調用函數append、len和cap的效果都是一樣的。nil切片和空切片底層結構如下:
      

      

2.3 切片增長

  切片的增長是通過調用append函數完成的。函數append總是會增加新切片的長度,而容量可能會改變,也可能不會改變,這取決於被操作切片的可用用量(注意:append不會修改傳入的切片,而是會返回一個新的切片)。

// 創建一個整型切片
// 其長度和容量都是5個元素
slice := []int{10, 20, 30, 40, 50}

// 創建一個新切片
// 其長度為2 個元素,容量為4個元素
newSlice := slice[1:3]

// 使用原有的容量來分配一個新元素
// 將新元素賦值為 60
newSlice = append(newSlice, 60)

fmt.Printf("slice: %v\n", slice)       // slice: [10 20 30 60 50]
fmt.Printf("newSlice: %v\n", newSlice) // newSlice: [20 30 60]

  以上代碼運行的底層結構如下圖:
      

  因為newSlice在底層數組里還有額外的容量可用,append操作將可用的元素合並到切片的長度,並對其進行賦值。由於和原始的slice共享同一個底層數組,所以slice中索引為3的元素的值也被改動 。如果切片的底層數組沒有足夠的容量可用,append函數會創建一個新的底層數組,將被引用的現有的值復制到新的數組里,再追加新的值。

// 創建一個整型切片
// 其長度和容量都是4個元素
slice := []int{10, 20, 30, 40}

// 向切片追加一個新元素
// 將新元素賦值為50
newSlice := append(slice, 50)

// 改變newSlice中的某個值,發現原始slice的值並沒有變化
newSlice[2] = 999

fmt.Printf("slice: %v\n", slice)       // slice: [10 20 30 40]
fmt.Printf("newSlice: %v\n", newSlice) // newSlice: [10 20 999 40 50]

  當這個append操作完成后,newSlice擁有一個全新的底層數組,這個數組的容量是原來的兩倍。
      

  函數append會智能地處理底層數組的容量增長。在切片的容量小於1000時,總是會成倍的增長容量。一旦元素個數超過1000,容量的增長因子會設為1.25,也就是每次增加25%的容量。

2.4 迭代切片

  切片可以用range迭代,但是要注意:如果只用一個值接收range,則得到的只是切片的下標,用兩個值接收range,則得到的才是下標和對應的值。

slice := []int{10, 20, 30, 40}

// 如果只用一個值接收range,則得到的只是切片的下標
for i := range slice {
	fmt.Println(i)
}

// 如果用兩個值接收range,則得到的是下標和對應的值
for i, v := range slice {
	fmt.Println(i, v)
}

  需要強調的是,range創建了每個元素的副本,而不是直接返回對該元素的引用。如果使用該值變量的地址作為指向每個元素的指針,就會造成錯誤。

slice := []int{10, 20, 30, 40}

/*
	下面的打印輸出如下:
	Value: 10, Value-Addr: C00000C168, ElemAddr: C000012560
	Value: 20, Value-Addr: C00000C168, ElemAddr: C000012568
	Value: 30, Value-Addr: C00000C168, ElemAddr: C000012570
	Value: 40, Value-Addr: C00000C168, ElemAddr: C000012578

	Value-Addr 表示的是遍歷時用到的變量 v
	ElemAddr 表示的是原來的切片slice里每個元素的地址
	可以看出 range 在遍歷時,將slice的每個元素都復制到了同一個變量 v 。
	使用閉包的時候,尤其要注意range的這種特性。
*/
for i, v := range slice {
	fmt.Printf("Value: %d, Value-Addr: %X, ElemAddr: %X\n",
		v, &v, &slice[i])
}

2.5 在函數間傳遞切片

  Go語言中參數的傳遞都是以值的方式傳遞的,引用類型也不例外。因為類型本身包裝的是一個指針,所以傳遞引用類型是把指針復制一份,而不會復制其底層數據結構。
      

 

3. 多維切片

  和多維數組類似。
 

4. 參考文獻

  《Go語言實戰》
  《Go語言學習筆記》


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM