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 mainimport "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 mainimport "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 mainimport "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 mainimport "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個字節的數據會非常快速、簡單。這也是切片效率高的地方。
不需要傳遞指針和處理復雜的語法,只需要復制切片。
?
轉載于:https://www.cnblogs.com/yangmingxianshen/p/10094582.html
總結
- 上一篇: 冒泡和二分排序算法
- 下一篇: stm32f103zet6实现HTTP协