Go 学习笔记(11):切片
概念
切片(slice)是對數組一個連續片段的引用,,所以切片是一個引用類型(更類似于 C/C++ 中的數組,Python 中的 list )。因為切片是引用,不需要使用額外的內存存儲并且比數組更有效率,所以在 Go 中 切片比數組更常用。
構成
一個 slice 由三個部分構成:指針、長度和容量:
- 指針:指向第一個 slice 元素對應的底層數組元素的地址(注意,slice 的第一個元素并不一定就是數組的第一個元素)
- 長度對應 slice 中元素的數目,不能超過容量
- 容量一般是從 slice 的開始位置到底層數組的結尾位置,內置的 len 和 cap 函數分別返回 slice 的長度和容量。
創建
數組切片
Slice 本身沒有數據,是對底層數組的 view。多個 slice 之間可以共享底層的數據,并且引用的數組部分區間可能重疊,即一個切片和相關數組的其他切片是共享存儲的。相反,不同的數組總是代表不同的存儲(數組實際上是切片的構建塊)。
arr := [...]int{0, 1, 2, 3, 4, 5, 6, 7} // [0 1 2 3 4 5 6 7] s := arr[2:6] // [2 3 4 5]// 共享存儲 s[0] = 10 fmt.Println(arr) // [0 1 10 3 4 5 6 7] fmt.Println((s)) // [10 3 4 5] 復制代碼數組和 slice 之間有著緊密的聯系,和數組不同的是,切片的長度可以在運行時修改,最小為 0 最大為相關數組的長度,切片是一個 長度可變的數組。
聲明賦值
切片與數組的類型字面量的唯一不同是不包含代表其長度的信息。因此,不同長度的切片值是有可能屬于同一個類型的,不同長度的數組值必定屬于不同類型。
s := []int{1, 2, 3} 復制代碼使用append
由于值傳遞的關系,必須接收 append 的返回值:
s = append(s, val) s = append(s, val1,val2, val3) s = append(s, slice...) 復制代碼var s []int for i :=0; i<10; i++ {s = append(s, i) } fmt.Println(s) 復制代碼使用make
cap 是可選參數:make([]type, len, cap)
s1 := make([]int, 16) s2 := make([]int, 10, 32) 復制代碼擴展閱讀 —— new() 和 make() 的區別: 兩者都在堆上分配內存,但是它們的行為不同,適用于不同的類型:
- new(T) 為類型 T 分配一片內存,初始化為 0 并且返回類型為 *T 的內存地址,返回一個指向類型為 T,值為 0 的地址的指針。適用于數組、結構體。
- make(T) 返回一個類型為 T 的初始值,它只適用于 3 種內建的引用類型:切片、map 和 channel。
也就是說,new 函數分配內存,make 函數初始化。
特性
長度len
切片元素的個數,使用內置函數 len 獲取。
容量cap
數組的容量是其長度,切片的容量是切片的第一個元素到底層數組的最后一個元素的長度。
如果切片操作超出 cap(s) 的上限將導致一個panic 異常,但是超出 len(s) 則是意味著擴展了 slice,新 slice 的長度會變大(請見添加元素 - 自動擴展cap)。
// 數組的容量是其長度 arr := [...]int{0, 1, 2, 3, 4, 5, 6, 7} fmt.Println(cap(arr)) // 8// 切片的容量 arr := [...]int{0, 1, 2, 3, 4, 5, 6, 7} s1 := arr[2:6] // [2 3 4 5] s2 := s1[3:5] // [5 6],注意,擴展了 fmt.Println(cap(s1)) // 6 fmt.Println(cap(s2)) // 3 復制代碼操作
添加元素
arr := [...]int{0, 1, 2, 3, 4, 5, 6, 7} s1 := arr[2:6] s2 := s1[3:5] s3 := append(s2, 10) s4 := append(s3, 11) s5 := append(s4, 12) fmt.Println(s1) // [2 3 4 5] fmt.Println(s2) // [5 6],注意,擴展了s1 fmt.Println(s3) // [5 6 10] fmt.Println(s4) // [5 6 10 11] fmt.Println(s5) // [5 6 10 11 12] fmt.Println(arr) // [0 1 2 3 4 5 6 10] 復制代碼觀察最后的 arr, 長度仍然是 7,說明 s3 的 append 改變了原 array。s4、s5 的 append 由于超出了底層數組的容量,實際上 s3、s4 不再 view 原 arr 了,而是 view 了個新的更加長的 array。
可見,添加元素時如果超越了 cap,系統會重新分配更大的底層數組。如果原數組不再被使用,會被垃圾回收。
自動擴展 cap:
var s []intfor i :=0; i<10; i++ {s = append(s, i)fmt.Println(len(s), cap(s)) } fmt.Println(s)// 1 1 // 2 2 // 3 4 // 4 4 // 5 8 // 6 8 // 7 8 // 8 8 // 9 16 // 10 16 // [0 1 2 3 4 5 6 7 8 9] 復制代碼刪除元素
刪除索引為 3 的元素,也可以使用 copy 實現,見模擬stack。
arr := [...]int{0, 1, 2, 3, 4, 5} s := arr[:] // [0 1 2 3 4 5]s = append(s[:3], s[4:]...) fmt.Println(s) // [0 1 2 4 5] 復制代碼切片拷貝
copy(dst slice, src slice):對第一個參數值進行修改。
兩個 slice 可以共享同一個底層數組,甚至有重疊也沒有問題。copy 函數將返回成功復制的元素的個數,等于兩個 slice 中較小的長度,所以我們不用擔心覆蓋會超出目標 slice 的范圍。
s1 := []int{1, 2} s2 := []int{4, 5, 6} copy(s1, s2) fmt.Println(s1) // [4 5]s1 := []int{1, 1, 1, 1} s2 := []int{4, 5, 6} copy(s1, s2) fmt.Println(s1) // [4 5 6 1] 復制代碼遍歷
for-range 結構遍歷切片
比較
和數組不同的是,slice 之間不能比較,因此我們不能使用 == 操作符來判斷兩個 slice 是否含有全部相等元素。
標準庫提供了高度優化的 bytes.Equal 函數來判斷兩個字節型 slice 是否相等([]byte),但是對于其他類型的 slice,我們必須自己展開每個元素進行比較:
func equal(x, y []string) bool {if len(x) != len(y) {return false}for i := range x {if x[i] != y[i] {return false}}return true } 復制代碼nil
slice 唯一合法的比較操作是和 nil 比較:
if slice1 == nil { /* ... */ } 復制代碼- 一個零值的 slice 等于 nil
- 一個 nil 值的 slice 并沒有底層數組
- 一個 nil 值的 slice 的長度和容量都是 0,但是也有非 nil 值的 slice 的長度和容量也是 0 的,例如 []int{} 或 make([]int, 3)[3:]
- 一個 nil 值的 slice 的行為和其它任意 0 長度的 slice 一樣,所有 Go 語言函數應該同等式對待 nil 值的 slice 和 0 長度的 slice。
- 如果需要測試一個 slice 是否是空的,使用 len(s) == 0 來判斷,不要用 s == nil 來判斷
函數傳參
如果一個函數需要對數組操作,最好把參數聲明為切片。當調用函數時,把數組分片,傳入切片引用。
數組元素和
package mainimport "fmt"func sum(a []int) int {s := 0for i := range a {s += i}return s }func main() {arr := [...]int{0, 1, 2, 3, 4, 5}fmt.Println(sum(arr[:])) // 15var s1 []intfmt.Println(s1 == nil, sum(s1)) // true 0s2 := []int{}fmt.Println(s2 == nil, sum(s2)) // false 0 }復制代碼反轉數組
package mainimport "fmt"func reverse(s []int) {// for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 {// s[i], s[j] = s[j], s[i]// }for index, i := range s {s[index], s[len(s) - i - 1] = s[len(s) - i - 1], s[index]} }func main() {arr := [...]int{0, 1, 2, 3, 4, 5}reverse(arr[:])fmt.Println(arr)var s1 []intreverse(s1)fmt.Println(s1)s2 := []int{}reverse(s2)fmt.Println(s2) } 復制代碼應用技巧
去除空值
舉個例子,去除切片中的空值。注意,輸入的 slice 和輸出的 slice 共享同一底層數組,這有可能修改了原來的數組。這是重用原來的 slice ,節約了內存。
package mainimport ("fmt" )func nonempty(strings []string) []string {i := 0for _, s := range strings {if s != "" {strings[i] = si++}}return strings[:i] }func main() {data := []string{"one", "", "three"}fmt.Printf("%q\n", nonempty(data)) // ["one" "three"]fmt.Printf("%q\n", data) // ["one" "three" "three"],改變原數組 } 復制代碼使用 append 函數實現:
func nonempty2(strings []string) []string {out := strings[:0] // 注意,重用原 slice 的關鍵for _, s := range strings {if s != "" {out = append(out, s)}}return out } 復制代碼模擬棧
上面的 nonempty2 函數是用 slice 模擬一個 stack,最初給定的空 slice 對應一個空的 stack:
插入元素(push):
stack = append(stack, v) 復制代碼取出最頂部(slice的最后)的元素:
top := stack[len(stack)-1] 復制代碼通過收縮 stack 彈出棧頂的元素(pop):
stack = stack[:len(stack)-1] // pop 復制代碼刪除 slice 中間的某個元素并保存原有的元素順序:
func remove(slice []int, i int) []int {copy(slice[i:], slice[i+1:])return slice[:len(slice)-1]// 或者// return append(slice[:i], slice[i+1:]...) } 復制代碼深入理解
appendInt
append 函數對于理解 slice 底層是如何工作的非常重要。下面是第一個版本的 appendInt 函數,專門用于處理 []int 類型的 slice:
func appendInt(x []int, y int) []int {var z []intzlen := len(x) + 1if zlen <= cap(x) {// There is room to grow. Extend the slice.z = x[:zlen]} else {// There is insufficient space. Allocate a new array.// Grow by doubling, for amortized linear complexity.zcap := zlenif zcap < 2*len(x) {zcap = 2 * len(x)}z = make([]int, zlen, zcap)copy(z, x) // a built-in function; see text}z[len(x)] = yreturn z } 復制代碼append
內置的 append 函數可能比 appendInt 的內存擴展策略更復雜,通常我們并不知道 append 調用是否導致了內存的重新分配,所以也不能確認新的 slice 和原始的 slice 是否引用的是相同的底層數組空間。此時,我們就不能確認在原先的 slice 上的操作是否會影響到新的 slice。在這種情況下,通常是將 append 返回的結果直接賦值給輸入的 slice 變量:s = append(s, val)。
更新 slice 變量不僅對調用 append 函數是必要的,實際上對應任何可能導致長度、容量或底層數組變化的操作都是必要的。要正確地使用 slice,需要記住盡管底層數組的元素是間接訪問的,但是 slice 對應結構體本身的指針、長度和容量部分是直接訪問的。要更新這些信息需要像上面例子那樣一個顯式的賦值操作。從這個角度看,slice 并不是一個純粹的引用類型,它實際上是一個類似下面結構體的聚合類型:
type IntSlice struct {ptr *intlen, cap int } 復制代碼內置的 append 函數可以追加多個元素,甚至追加一個 slice:
var x []int x = append(x, 1) x = append(x, 2, 3) x = append(x, 4, 5, 6) x = append(x, x...) // append the slice x 復制代碼修改 appendInt,實現類似 append 函數的功能:
func appendInt(x []int, y ...int) []int {var z []intzlen := len(x) + len(y)// ...expand z to at least zlen...copy(z[len(x):], y)return z } 復制代碼參考目錄
- Go語言圣經中文版
- Go 入門指南
總結
以上是生活随笔為你收集整理的Go 学习笔记(11):切片的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 你的指环
- 下一篇: vscode插件开发实践与demo源码