Golang數(shù)據(jù)類型之切片
目錄
1、切片介紹
2、聲明和初始化
2.1 make 創(chuàng)建
2.2 字面量創(chuàng)建
2.3 創(chuàng)建數(shù)組和切片的區(qū)別
2.4 創(chuàng)建切片的本質(zhì)
3、切片訪問
4、nil 和空切片
5、切片中添加元素
6、通過切片創(chuàng)建切片
7、切片遍歷
8、切片拷貝
9、切片作為函數(shù)參數(shù)

1、切片介紹
Go中的切片slice依賴于數(shù)組,它的底層就是數(shù)組,所以數(shù)組具有的優(yōu)點,slice都有。且slice支持可以通過append向slice中追加元素,長度不夠時會動態(tài)擴展,通過再次slice切片,可以得到得到更小的slice結(jié)構(gòu),可以迭代、遍歷等
// runtime/slice.go
type slice struct {
array unsafe.Pointer // 數(shù)組指針
len int // 長度
cap int // 容量
}
每一個slice結(jié)構(gòu)都由 3 部分組成:
容量(capacity):即底層數(shù)組的長度,表示這個 slice目前最多能擴展到的長度長度(length):表示 slice當前的長度,即當前容納的元素個數(shù)數(shù)組指針(array):指向底層數(shù)組的指針
比如創(chuàng)建一個長度為3,容量為5,int類型的切片
s := make([]int, 3, 4)
fmt.Println(a, len(s), cap(s)) // [0 0 0] 3 5
2、聲明和初始化
在Go中可以通過多種方式創(chuàng)建和初始化切片
是否提前知道切片所需的容量通常會決定如何創(chuàng)建切片
2.1 make 創(chuàng)建
// 創(chuàng)建一個整型切片, 其長度為 3 個元素,容量為 5 個元素
slice := make([]int, 3, 5)
// 我們也可以省略容量, 默認長度==容量
// 創(chuàng)建一個整型切片 其長度和容量都是 5 個元素
slice := make([]int, 5)
// 但是長度不能小于容量, 否則編譯器過不了
// a := make([]int, 5, 3)
2.2 字面量創(chuàng)建
// 這種方法和創(chuàng)建數(shù)組類似,只是不需要指定[]運算符里的值。初始的長度和容量會基于初始化時提供的元素的個數(shù)確定
slice := []int{1,2,3}
// 和數(shù)組一樣也可以通過指定索引初始化, 比如index 4 值為100
slice := []int{3: 100}
2.3 創(chuàng)建數(shù)組和切片的區(qū)別
如果在[]運算符里指定了一個值,那么創(chuàng)建的就是數(shù)組而不是切片,比如
a := [3]int{1,2,3}
b := []int{1,2,3}
雖然他們聲明時只要這一點點區(qū)別,但是他們的數(shù)據(jù)結(jié)構(gòu)區(qū)差別卻很大,一個是引用類型一個是值類型
2.4 創(chuàng)建切片的本質(zhì)
切片相關(guān)源碼放置位置: src/runtime/slice.go, 我們使用make時, 實際上是調(diào)用的makeslice函數(shù)
// 這里一波操作過后返回的是slice的pointer
func makeslice(et *_type, len, cap int) unsafe.Pointer {}
3、切片訪問
對切片里某個索引指向的元素賦值和對數(shù)組里某個索引指向的元素賦值的方法完全一樣。使
用[]操作符就可以改變某個元素的值,下面是使用切片字面量來聲明切片
s := []int{1,2,3}
s[0]
// 但是不能越界訪問, 比如
s[3] // panic: runtime error: index out of range [3] with length 3
查看切片長度: len
查看切片容量: cap
4、nil 和空切片
聲明未初始化的切片為nil
var s []int
var s1 []int
fmt.Printf("%p\n", s1) // 0x0
make初始化的是一個空切片
s := make([]int,0)
// unsafe.Pointer ——> *slice
s2 := make([]int, 0)
fmt.Printf("%p\n", s2) // 0x126c9
所以nil切片直接賦值是要報錯的
var s []int
s[0] = 1 // panic: runtime error: index out of range [0] with length 0
5、切片中添加元素
通過append函數(shù)往切片中追加元素, 比如
s := make([]int, 0, 4)
s = append(s, 10, 20, 30, 40)
現(xiàn)在底層數(shù)組已經(jīng)滿了,再往里面追加元素會如何?
s = append(s,50)
函數(shù)append()會智能地處理底層數(shù)組的容量增長。在切片的容量小于1024個元素時,總是會成倍地增加容量。一旦元素個數(shù)超過1024,容量的增長因子會設(shè)為1.25,也就是會每次增加25%的容量(隨著語言的演化,這種增長算法可能會有所改變)
因此擴容對于切片來說是一個比較消耗成本的事情,會開辟新的內(nèi)存空間
擴容時是新創(chuàng)建一個切片數(shù)組,若原數(shù)據(jù)沒有繼續(xù)使用,會被gc
s1 := make([]int, 0, 4)
s1 = append(s1, 10, 20, 30, 40) // 10, 20, 30, 40
fmt.Println(s1, len(s1), cap(s1)) // [10 20 30 40] 4 4
s1 = append(s1, 50)
fmt.Println(s1, len(s1), cap(s1)) // [10 20 30 40 50] 5 8
6、通過切片創(chuàng)建切片
切片之所以被稱為切片,是因為創(chuàng)建一個新的切片,也就是把底層數(shù)組切出一部分。通過切片創(chuàng)建新切片的語法如下, 詳情請參考: 切片的語法
slice[low : high]
slice[low : high : max]
low : 表示從 slic的第幾個元素開始切high : 控制切片的長度 high-lowmax : 控制切片的容量 max-low
比如
s1 := []int{1, 2, 3, 4}
s2 := s1[2:4:4] // [index2, index4) 左閉右開區(qū)間, 容量 4-2
fmt.Println(s2, len(s2), cap(s2)) // [3 4] 2 2
如果high == max也可以省略max,比如:
s3 := s1[2:4]
再次基礎(chǔ)上還要幾種省略寫法:
省略 low:表示從 index 0開始省略 high:表示到結(jié)尾 len省略 max :表示到結(jié)尾 len都省略:等于復制
slice[i:] // 從 i 切到最尾部
slice[:j] // 從最開頭切到 j(不包含 j)
slice[:] // 從頭切到尾,等價于復制整個 slice
注意: 通過切片創(chuàng)建出來的切片是共享底層數(shù)據(jù)結(jié)構(gòu)的(數(shù)組)
共享底層數(shù)組會導致相互影響, 比如修改原切片會影響多所有復制出來的切片
s1 := []int{10, 20, 30, 40}
s2 := s1[1:3]
fmt.Println(s2, len(s2), cap(s2))
fmt.Println(s1[1], s2[0])
s1[1] = 200
fmt.Println(s1[1], s2[0])
有擴容的原理也可以知道,當擴容后,就不共享底層數(shù)組了,比如:
s1 := []int{10, 20, 30, 40}
s2 := s1[1:3:3]
fmt.Println(s2, len(s2), cap(s2))
fmt.Println(s1[1], s2[0])
s2 = append(s2, 30) // s2 擴容
s1[1] = 200 // 修改s1
fmt.Println(s1[1], s2[0]) // s1修改并不會影響s2
因此,一般不要修改切片,如果要修改請使用后面的深拷貝復制一個全新的切片
7、切片遍歷
切片是一個集合,可以迭代其中的元素。Go有個特殊的關(guān)鍵字range,它可以配合關(guān)鍵字for來迭代切片里的元素
func TestSliceAppend1(t *testing.T) {
s := make([]int, 0, 4)
s = append(s, 10, 20, 30, 40)
for i, v := range s {
fmt.Println(i, v)
}
/*
0 10
1 20
2 30
3 40
*/
}
這種方式底層的實現(xiàn),也是拷貝一份切片提供給循環(huán)使用,因此同樣會帶來開銷
當?shù)衅瑫r,關(guān)鍵字range會返回兩個值。第一個值是當前迭代到的索引位置,第二個值是該位置對應元素值的一份副本。需要強調(diào)的是,range創(chuàng)建了每個元素的副本,而不是直接返回對該元素的引用。要想獲取每個元素的地址,可以使用切片變量和索引值
8、切片拷貝
不能像數(shù)組一樣直接使用賦值語句來拷貝一個切片,因為數(shù)組是值,而切片是指針, 真正的數(shù)據(jù)維護在底層數(shù)組里面
a1 := [2]{1,2}
a2 := a1 // 值拷貝, a1, a2 互不影響
s1 := []{1, 2}
s2 := s1 // 指針拷貝 s1, s2 指向同一*slice結(jié)構(gòu)體, 就是一個東西,等于沒拷貝
Go內(nèi)置的copy()函數(shù)可以將一個切片中的元素拷貝到另一個切片中,其函數(shù)聲明為
func copy(dst, src []Type) int
它表示把切片src中的元素拷貝到切片dst中,返回值為拷貝成功的元素個數(shù)。如果src比dst長,就截斷;如果src比dst短,則只拷貝src那部分
s1 := []int{10, 20, 30, 40}
s2 := make([]int, 5)
num := copy(s2, s1) // 這時候s1 和 s2 就是2個切片,包含底層數(shù)據(jù), 互不影響
fmt.Println(num) // 4
fmt.Println(s1, s2) // [10 20 30 40] [10 20 30 40 0]
s1[0] = 100
fmt.Println(s1[0], s2[0]) // 100 10
9、切片作為函數(shù)參數(shù)
函數(shù)在調(diào)用傳參時,都是值拷貝
切片的本質(zhì)是指針,如果是切片作為函數(shù)的參數(shù)調(diào)用,則拷貝的是指針的地址
因此切片作為函數(shù)的參數(shù)時,最大的好處是傳遞效率高
因此切片的用法遠多于數(shù)組,數(shù)組用來定義底層的數(shù)據(jù)結(jié)構(gòu)
func TestSliceMain2(t *testing.T) {
s1 := make([]int, 0, 4)
s1 = append(s1, 10, 20, 30, 40) // 10, 20, 30, 40
fmt.Println(Sum1(s1)) // 100
}
func Sum1(args []int) int {
sum := 0
for _, v := range args {
sum += v
}
return sum
}
