1. 程式人生 > >go——切片(二)

go——切片(二)

切片是一種資料結構,這種資料結構便於使用和管理資料集合。

切片是圍繞動態陣列的概念構建的,可以按需自動增長和縮小。

切片的動態增長是通過內建函式append來實現的。這個函式可以快速且高效地增長切片。

還可以通過對切片再次切片來縮小一個切片地大小。

因為切片的底層記憶體也是在連續中分配的,所以切片還能獲得索引、迭代以及垃圾回收優化的好處。

1.內部實現

切片是一個很小的物件,對底層陣列進行了抽象,並提供相關的操作方法。

切片有3個欄位的資料結構,這些資料結構包含Go語言需要操作底層陣列的元資料

這3個欄位分別是指向底層陣列的指標切片訪問的元素個數(即長度)和切片允許增長到的元素個數(即容量)

 

 

 

2.建立和初始化

Go語言中有幾種方法可以建立和初始化切片。是否能提前知道切片需要的容量通常會決定要如何建立切片。

(1)make和切片字面量

一種建立切片的方法是使用內建的make函式。當使用make時,需要傳入一個引數,指定切片的長度。

//使用長度宣告一個字串切片

//建立一個字串切片
//其長度和容量都是5個元素
slice := make([]string, 5)

如果只指定長度,那麼切片的容量和長度相等。也可以分別指定長度和容量

//使用長度和容量宣告整型切片

//建立一個整型切片
//長度為3,容量為5
slice := make([]int,3,5)  

分別指定長度和容量時,建立的切片,底層陣列的長度是指定的容量但初始化後並不能訪問所有的陣列元素

上述程式碼中的切片可以訪問3個元素,而底層陣列擁有5個元素。

剩餘的2個元素可以在後期操作中合併到切片,可以通過切片訪問這些元素。

如果基於這個切片建立新的切片,新切片會和原有的切片共享底層陣列,也能通過後期操作來訪問多餘容量的元素。

 

不允許建立容量小於長度的切片。

slice := make([]int, 4, 3)  //len larger than cap in make([]int)

 

另一種常用的建立切片的方法是使用切片字面量。

這種方法和建立陣列類似,只是不需要指定[]運算子裡的值。

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

//通過切片字面量來宣告切片

//建立字串切片
//其長度和容量都是5個元素
slice := []string{"red","pink",''yellow","blue"}

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

 

當使用切片字面量時,可以設定初始長度和容量。要做的就是在初始化時給出所需長度和容量作為索引。

//使用索引宣告切片

//建立字串切片
//使用空字串初始化第100個元素
slice := []string{99:""}

 

記住,如果在[]運算子裡指定了一個值,那麼建立的就是陣列而不是切片。

只有不指定值得時候才會建立切片。

//宣告陣列和宣告切片得不同

//建立有3個元素得整型陣列
array := [3]int{10,20,30}

//建立長度和容量都是3的整型切片
slice := []int{10,20,30}

  

 

(2)nil和空切片

 有時,程式可能需要宣告一個值為nil的切片(也稱nil切片),只需要在宣告時不做任何初始化,就會建立一個nil切片。

//建立nil整型切片
var slice []int

在Go語言裡,nil切片是很常見的建立切片的方法。

nil切片可以用於很多標準庫和內建函式。在需要描述一個不存在的切片時,nil切片會很好用。

例如,函式要求返回一個切片但是發生異常的時候。

 

利用初始化,通過宣告一個切片可以建立一個空切片。

//宣告空切片

//使用make建立空的整型切片
slice := make([]int, 0)

//使用切片字面量建立空的整型切片
slice := []int{}

空切片在底層陣列0個元素,也沒有分配任何儲存空間。

想表示空集合空切片很有用,例如資料庫查詢返回0個查詢結果時。

不管是使用nil切片還是空切片,對其呼叫內建函式append、len和cap的效果都是一樣的。

 

 

 

3.使用切片

(1)賦值和切片

對切片裡某個索引指向的元素賦值和對數組裡某個索引指向的元素賦值的方法完全一樣。

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

//改變索引為1的元素的值
slice[1] = 25

切片之所以被稱之為切片,是因為建立一個新的切片就是把底層陣列切除一部分

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


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

我們有兩個切片,它們共享一段底層陣列,但通過不同的切片會看到底層陣列不同的部分。

第一個切片slice能夠看到底層陣列全部5個元素的容量,不過之後的newSlice就不能看到。

對於newSlice,底層陣列的容量只有4個元素。newSlice無法訪問到它所指向的底層陣列的第一個元素之前。

所以對於newSlice來說,之前的那些元素就是不存在的。

如何計算長度和容量

對底層陣列容量是k的切片slice[i:j]
長度:j - i
容量:k - i

對底層陣列容量是5的切片slice[1:3]來說
長度: 3 - 1 = 2
容量: 5 - 1 = 4

需要記住的是,現在兩個切片共享同一個底層陣列

如果一個切片修改了該底層陣列的共享部分,另一個切片也能感知到

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

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

//修改newSlice索引為1的元素
//同時也修改了原來的slice的索引為2的元素
newSlice[1] = 35

切片只能訪問到其長度內的元素。試圖訪問長度超過其長度的元素將會導致語言執行異常。

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

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

//修改newSlice索引為3的元素
//這個元素對於newSlice來說不存在
newSlice[3] = 45  //index out of range  

切片有額外的容量是很好的,但是如果不能把這些容量合併到切片的長度裡,這些容量就沒有用處。

好在可以用Go語言內建函式append來做這種合併很容易。

 

(2)切片增長

相對於陣列而言,使用切片的一個好處是,可以按需增加切片的容量。

Go語言內建的append函式會處理增加長度時的所有操作細節。

要使用append,需要一個被操作的切片和一個追加的值。

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

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

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

當append呼叫返回時,會返回一個包含修改結果的新切片。

函式append總是會增加新切片的長度,而容量有可能會改變,也可能不變,這取決於被操作的切片的可用容量。

因為newSlice在底層數組裡還有額外的容量可用,append操作將可用的元素合併到切片的長度,並對其進行賦值。

由於和原始的slice共享一個底層陣列,slice中索引為3的元素的值也被改動了。

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

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

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

 當這個append操作完成後,newSlice擁有一個全新的底層陣列,這個陣列的容量原來的兩倍。

函式append會智慧的處理底層陣列的容量增長,在切片的容量小於1000個元素時,總是會成倍的增加容量。

一旦元素個數超過1000,容量的增長因子會設為1.25,也就是會每次增加25%的容量。

 

 

(3)建立切片時的3個索引

在建立切片時,還可以使用之前我們沒有提及的第三個索引選項。

第三個索引可以用來控制新切片的容量。其目的並不是要增加容量,而是要限制容量。

可以看到,允許限制新切片的容量為底層資料提供了一定的保護,可以更好的控制追加操作。

//建立字串切片
//其長度和容量都是5個長度
source := []string{"apple",  "orange",  "plum",  "banana",  "grape"}

使用第三索引來完成切片操作。

//將第三個元素切片並限制容量
//其長度為1個元素,容量為2個元素
slice := source[2:3:4]

這個切片操作執行後,新切片裡從底層陣列引用了1個元素,容量是2個元素。

具體來說,新切片引用了plum元素,並將容量擴充套件到banana元素。

我們應用之前定義的公式來計算新切片的長度和容量。

對於slice[i:j:k] 或 slice[2:3:4]
長度: j -  i 或 3 - 2 = 1
容量: k - i 或 4 - 2 = 2

 

如果試圖設定的容量比可用容量還大,就會得到一個語言執行時錯誤。

//這個切片操作試圖設定容量為4
//這比可用容量大
slice := source[2:3:6]  //slice bounds out of range

內建函式append會首先使用可用容量,一旦沒有可用容量,會分配一個新的底層陣列。

這導致很容易忘記切片間正在共享同一個底層陣列。

一旦發生這種情況,對切片進行修改,很可能會導致隨機且奇怪的問題。

對切片內容的修改會影響多個切片,卻很難找到問題的原因。

如果在切片時設定切片的容量和長度一樣,就可以強制讓新切片的第一個append操作建立新的底層陣列,與原有底層陣列分離。

新切片與原有的底層陣列分離後,可以安全的進行後續修改。

//建立字串切片
//其長度和容量都是5個長度
source := []string{"apple",  "orange",  "plum",  "banana",  "grape"}

//對第三個元素做切片,並限制容量
//其長度和容量都是一個長度
slice := source[2:3:3]

//向slice追加新字串
slice = append(slice, "Kiwi")

如果不加第三個索引,由於剩餘的所有容量都屬於slice,向slice追加kiwi會改變原有底層陣列索引為3的元素的值Banana。

當我們限制slice容量為1的時候,再進行append操作的時候會建立一個新的底層陣列,

這個陣列包括兩個元素,並將水果pium複製進來,再追加新水果Kiwi,並返回一個引用了這個底層陣列的新切片。

因為新的切片slice擁有了自己的底層陣列,所以杜絕了可能發生的問題。

內建函式append也是一個可變引數的函式。這意味著可以在一次呼叫傳遞多個追加多個值。

如果使用...運算子,可以將一個切片的所有元素追加到另一個切片裡。

//建立兩個切片,並分別用兩個整數進行初始化
s1 := []int{1, 2}
s2 := []int{3, 4}

//將兩個切片追加到一起
fmt.Println(append(s1, s2...)) //[1 2 3 4]

 切片s2裡的所有值都追加到了切片s1的後面。

 

 

(4)迭代切片

 既然切片是一個集合,可以迭代其中的元素。Go語言有個特殊的關鍵字range,它可以配合關鍵字for來迭代切片裡的元素。

package main

import "fmt"

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

	//迭代每一個元素
	for index, value := range slice {
		fmt.Printf("Index: %d Value: %d\n", index, value)
	}
}

/*
Index: 0 Value: 10
Index: 1 Value: 20
Index: 2 Value: 30
Index: 3 Value: 40
*/

當迭代切片時,關鍵字range會返回兩個值。

 

第一個值是當前迭代到的索引位置,第二個值是該位置對應元素值得一個副本。

需要強調得是,range建立了每個元素的副本,而不是直接返回該元素的引用。

如果使用該值變數的地址作為每個元素的指標,就會造成錯誤。這是為什麼了?

package main

import "fmt"

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

	//迭代每一個元素,並顯式值和地址
	for index, value := range slice {
		fmt.Printf("Value: %d Value-Addr: %X ElemAddr: %X\n", value, &value, &slice[index])
	}
}

/*
Value: 10 Value-Addr: C00000A168 ElemAddr: C00000E480
Value: 20 Value-Addr: C00000A168 ElemAddr: C00000E488
Value: 30 Value-Addr: C00000A168 ElemAddr: C00000E490
Value: 40 Value-Addr: C00000A168 ElemAddr: C00000E498
 */

因為迭代返回的變數是一個迭代過程中根據切片依次賦值的新變數,所以Value的地址總是相同的。

要想獲取每個元素的地址,可以使用切片變數和索引值。

如果不需要索引值,可以使用空白佔位符來忽略這個值。

package main

import "fmt"

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

	//迭代每一個元素
	for _, value := range slice {
		fmt.Printf("Value: %d\n", value)
	}
}

/*
Value: 10
Value: 20
Value: 30
Value: 40
*/

關鍵字range總是會從切片頭部開始迭代。如果想對迭代進行控制,依舊可以使用傳統的for迴圈。

package main

import "fmt"

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

	//迭代每一個元素
	for index := 2; index < len(slice); index++ {
		fmt.Printf("Index: %d Value: %d\n", index, slice[index])
	}
}

/*
Index: 2 Value: 30
Index: 3 Value: 40
*/

有兩個特殊的內建函式len和cap,可以用於處理陣列、切片和通道。

對於切片,函式len返回切片的長度,函式cap返回切片的容量。

 

 

 

4.多維切片

和陣列一樣,切片也是一維的。不過可以組合多個切片形成多維切片。

//建立一個整型切片的切片
slice := [][]int{{10}, {100, 200}}

我們有了一個包含兩個元素的外層切片,每個元素包含一個內層的整型切片。

 

外層切片包含兩個元素,每一個元素都是一個切片。

這種組合可以讓使用者建立非常負責且強大的資料結構。同時append的規則也可以用到組合後的切片上。

//建立一個整型切片的切片
slice := [][]int{{10}, {100, 200}}

//為第一個切片追加值為20的元素
slice[0] = append(slice[0], 20)

Go語言裡使用append函式處理追加的方式很簡明:先增長切片,再將新的整型切片賦值給外層切片的第一個元素。

 

即使是這麼簡單的多維切片,操作時也會涉及眾多佈局和值。

不過切片本身結構很簡單,可以以很小的成本再函式間傳遞。

 

 

 

5.在函式間傳遞切片

 在函式間傳遞切片就是要在函式間以值得方式傳遞切片。

由於切片的尺寸很小,在函式間複製和傳遞切片成本也很低。

//分配包含100萬個整型值得切片
slice := make([]int, le6)

//將slice傳遞給函式foo
slice = foo(slice)

//函式foo接受一個整型切片,並返回這個切片
func foo(slice []int) []int{
         ...
    return slice  
}

  

在64位架構的機器上,一個切片需要24位元組的記憶體:指標欄位需要8個位元組,長度和容量分別需要8個位元組。

由於與切片關聯的資料包含在底層數組裡,不屬於切片本身,所以說將切片複製到任意函式的時候,對底層陣列大小都不會影響。

複製時只會複製切片本身,不會涉及底層陣列。

在函式間傳遞24個位元組的資料會非常快速、簡單。這也是切片效率高的地方。

不需要傳遞指標和處理複雜的語法,只需要複製切片。