go slice 數(shù)組和切片使用區(qū)別示例解析
正文
slice(切片)是 go 里面非常常用的一種數(shù)據(jù)結(jié)構(gòu),它代表了一個(gè)變長的序列,序列中的每個(gè)元素都有相同的數(shù)據(jù)類型。 一個(gè) slice 類型一般寫作 []T,其中 T 代表 slice 中元素的類型;slice 的語法和數(shù)組很像,但是 slice 沒有固定長度。
數(shù)組和切片的區(qū)別
數(shù)組有確定的長度,而切片的長度不固定,并且可以自動(dòng)擴(kuò)容。
數(shù)組的定義
go 中定義數(shù)組的方式有如下兩種:
- 指定長度:
arr := [3]int{1, 2, 3}
- 不指定長度,由編譯器推導(dǎo)出數(shù)組的長度:
arr := [...]{1, 2, 3}
上面這兩種定義方式都定義了一個(gè)長度為 3 的數(shù)組。正如我們所見,長度是數(shù)組的一部分,定義數(shù)組的時(shí)候長度已經(jīng)確定下來了。
切片的定義
切片的定義方式跟數(shù)組很像,只不過定義切片的時(shí)候不用指定長度:
s := []int{1, 2, 3}
在上面定義切片的代碼中,我們可以看到其實(shí)跟數(shù)組唯一的區(qū)別就是少了個(gè)長度。 那其實(shí)我們可以把切片看作是一個(gè)無限長度的數(shù)組。 當(dāng)然,實(shí)際上它并不是無限的,它只是在切片容納不下新的元素的時(shí)候,會(huì)自動(dòng)進(jìn)行擴(kuò)容,從而可以容納更多的元素。
數(shù)組和切片的相似之處
正如我們上面看到的那樣,數(shù)組和切片兩者其實(shí)非常相似,在實(shí)際使用中,它們也是有些類似的。
比如,通過下標(biāo)來訪問元素:
arr := [3]int{1, 2, 3}
// 通過下標(biāo)訪問
fmt.Println(arr[1]) // 2
s := []int{1, 2, 3}
// 通過下標(biāo)訪問
fmt.Println(s[1]) // 2
數(shù)組的局限
我們知道了,數(shù)組的長度是固定的,這也就意味著如果我們想往數(shù)組里面增加一個(gè)元素會(huì)比較麻煩, 我們需要新建一個(gè)更大的數(shù)組,然后將舊的數(shù)據(jù)復(fù)制過去,然后將新的元素寫進(jìn)去,如:
// 往數(shù)組 arr 增加一個(gè)元素:4
arr := [3]int{1, 2, 3}
// 新建一個(gè)更大容量的數(shù)組
var arr1 [4]int
// 復(fù)制舊數(shù)組的數(shù)據(jù)
for i := 0; i < len(arr); i++ {
arr1[i] = arr[i]
}
// 加入新的元素:4
arr1[3] = 4
fmt.Println(arr1)
這樣一來就非常的繁瑣,如果我們使用切片,就可以省去這些步驟:
// 定義一個(gè)長度為 3 的數(shù)組
arr := [3]int{1, 2, 3}
// 從數(shù)組創(chuàng)建一個(gè)切片
s := arr[:]
// 增加一個(gè)元素
s = append(s, 4)
fmt.Println(s)
因?yàn)閿?shù)組固定長度的缺點(diǎn),實(shí)際使用中切片會(huì)使用得更加普遍。
重新理解 slice
在開始之前,我們來看看 slice 這個(gè)單詞的意思:作為名詞,slice 的意思有 片;部分;(切下的食物)薄片;,作為動(dòng)詞,slice 的意思有 切;把…切成(薄)片; 的意思。 從這個(gè)角度出發(fā),我們可以把 slice 理解為從某個(gè)數(shù)組上 切下來的一部分(從這個(gè)角度看,slice 這個(gè)命名非常的形象)。我們可以看看下圖:

在這個(gè)圖中,A 是一個(gè)保存了數(shù)字 1~7 的 slice,B 是從 A 中 切下來的一部分,而 B 只包含了 A 中的一部分?jǐn)?shù)據(jù)。 我們可以把 B 理解為 A 的一個(gè) 視圖,B 中的數(shù)據(jù)是 A 中的數(shù)據(jù)的一個(gè) 引用,而不是 A 中數(shù)據(jù)的一個(gè) 拷貝 (也就是說,我們修改 B 的時(shí)候,A 中的數(shù)據(jù)也會(huì)被修改,當(dāng)然會(huì)有例外,那就是 B 發(fā)生擴(kuò)容的時(shí)候,再去修改 B 的話就影響不了 A 了)。
slice 的內(nèi)存布局
現(xiàn)在假設(shè)我們有如下代碼:
// 創(chuàng)建一個(gè)切片,長度為 3,容量為 7 var s = make([]int, 3, 7) s[0] = 1 s[1] = 2 s[2] = 3 fmt.Println(s)
對(duì)應(yīng)的內(nèi)存布局如下:

說明:
slice底層其實(shí)也是數(shù)組,但是除了數(shù)組之外,還有兩個(gè)字段記錄切片的長度和容量,分別是len和cap。- 上圖中,
slice中的array就是切片的底層數(shù)組,因?yàn)樗拈L度不是固定的,所以使用了指針來保存,指向了另外一片內(nèi)存區(qū)域。 len表明了切片的長度,切片的長度也就是我們可以操作的下標(biāo),上面的切片長度為3,這也就意味著我們切片可以操作的下標(biāo)范圍是0~2。超出這個(gè)范圍的下標(biāo)會(huì)報(bào)錯(cuò)。cap表明了切片的容量,也就是切片擴(kuò)容之前可以容納的元素個(gè)數(shù)。
切片容量存在的意義
對(duì)于我們?nèi)粘i_發(fā)來說,slice 的容量其實(shí)大多數(shù)時(shí)候不是我們需要關(guān)注的點(diǎn),而且由于容量的存在,也給開發(fā)者帶來了一定的困惑。 那么容量存在的意義是什么呢?意義就在于避免內(nèi)存的頻繁分配帶來的性能下降(容量也就是提前分配的內(nèi)存大小)。
比如,假如我們有一個(gè)切片,然后我們知道需要往它里面存放 1w 個(gè)元素, 如果我們不指定容量的話,那么切片就會(huì)在它存放不下新的元素的時(shí)候進(jìn)行擴(kuò)容, 這樣一來,可能在我們存放這 1w 個(gè)元素的時(shí)候需要進(jìn)行多次擴(kuò)容, 這也就意味著需要進(jìn)行多次的內(nèi)存分配。這樣就會(huì)影響應(yīng)用的性能。
我們可以通過下面的例子來簡單了解一下:
// Benchmark1-20 100000000 11.68 ns/op
func Benchmark1(b *testing.B) {
var s []int
for i := 0; i < b.N; i++ {
s = append(s, 1)
}
}
// Benchmark2-20 134283985 7.482 ns/op
func Benchmark2(b *testing.B) {
var s []int = make([]int, 10, 100000000)
for i := 0; i < b.N; i++ {
s = append(s, 1)
}
}
在第一個(gè)例子中,沒有給 slice 設(shè)置容量,這樣它就只會(huì)在切片容納不下新元素的時(shí)候才會(huì)進(jìn)行擴(kuò)容,這樣就會(huì)需要進(jìn)行多次擴(kuò)容。 而第二個(gè)例子中,我們先給 slice 設(shè)置了一個(gè)足夠大的容量,那么它就不需要進(jìn)行頻繁擴(kuò)容了。
最終我們發(fā)現(xiàn),在給切片提前設(shè)置容量的情況下,會(huì)有一定的性能提升。
切片常用操作
創(chuàng)建切片
我們可以從數(shù)組或切片生成新的切片:
注意:生成的切片不包含 end。
target[start:end]
說明:
target表示目標(biāo)數(shù)組或者切片start對(duì)應(yīng)目標(biāo)對(duì)象的起始索引(包含)end對(duì)應(yīng)目標(biāo)對(duì)象的結(jié)束索引(不包含)
如:
s := []int{1, 2, 3}
s1 := s[1:2] // 包含下標(biāo) 1,不包含下標(biāo) 2
fmt.Println(s1) // [2]
arr := [3]int{1, 2, 3}
s2 := arr[1:2]
fmt.Println(s2) // [2]
在這種初始化方式中,我們可以省略 start:
arr := [3]int{1, 2, 3}
fmt.Println(arr[:2]) // [1, 2]
省略 start 的情況下,就是從 target 的第一個(gè)元素開始。
我們也可以省略 end:
arr := [3]int{1, 2, 3}
fmt.Println(arr[1:]) // [2, 3]
省略 end 的情況下,就是從 start 索引處的元素開始直到 target 的最后一個(gè)元素處。
除此之外,我們還可以指定新的切片的容量,通過如下這種方式:
target[start:end:cap]
例子:
arr := [10]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
s := arr[1:4:5]
fmt.Println(s, len(s), cap(s)) // [2 3 4] 3 4
往切片中添加元素
我們前面說過了,如果我們想往數(shù)組里面增加元素,那么我們必須開辟新的內(nèi)存,將舊的數(shù)組復(fù)制過去,然后才能將新的元素加入進(jìn)去。
但是切片就相對(duì)簡單,我們可以使用 append 這個(gè)內(nèi)置函數(shù)來往切片中加入新的元素:
var a []int
a = append(a, 1) // 追加1個(gè)元素
a = append(a, 1, 2, 3) // 追加多個(gè)元素
a = append(a, []int{1,2,3}...) // 追加一個(gè)切片
切片復(fù)制
go 有一個(gè)內(nèi)置函數(shù) copy 可以將一個(gè)切片的內(nèi)容復(fù)制到另外一個(gè)切片中:
copy(dst, src []int)
第一個(gè)參數(shù) dst 是目標(biāo)切片,第二個(gè)參數(shù) src 是源切片,調(diào)用 copy 的時(shí)候會(huì)把 src 的內(nèi)容復(fù)制到 dst 中。
示例:
var a []int
var b []int = []int{1, 2, 3}
// a 的容量為 0,容納不下任何元素
copy(a, b)
fmt.Println(a) // []
a = make([]int, 3, 3) // 給 a 分配內(nèi)存
copy(a, b)
fmt.Println(a) // [1 2 3]
需要注意的是,如果 dst 的長度比 src 的長度小,那么只會(huì)截取 src 的前面一部分。
從切片刪除元素
雖然我們往切片追加元素的操作挺方便的,但是要從切片刪除元素就相對(duì)麻煩一些了。go 語言本身沒有提供從切片刪除元素的方法。 如果我們要?jiǎng)h除切片中的元素,只有構(gòu)建出一個(gè)新的切片:

對(duì)應(yīng)代碼:
var a = make([]int, 7, 7)
for i := 0; i < 7; i++ {
a[i] = i + 1
}
fmt.Println(a) // [1 2 3 4 5 6 7]
var b []int
b = append(b, a[:2]...) // [1 2]
b = append(b, a[5:]...) // [1 2 6 7]
fmt.Println(b) // [1 2 6 7]
在這個(gè)例子中,我們想從 a 中刪除 3、4、5 這三個(gè)元素,也就是下標(biāo) 2~4 的元素, 我們的做法是,新建了一個(gè)新的切片,然后將 3 前面的元素加入到這個(gè)新的切片中, 再將 5 后面的元素加入到這個(gè)新切片中。
最終得到的切片就是刪除了 3、4、5 三個(gè)元素之后的切片了。
切片的容量到底是多少?
假設(shè)我們有如下代碼:
var a = make([]int, 7, 7)
for i := 0; i < 7; i++ {
a[i] = i + 1
}
// [1 2 3 4 5 6 7]
fmt.Println(a)
s1 := a[:3]
// [1 2 3] 3 7
fmt.Println(s1, len(s1), cap(s1))
s2 := a[4:6]
// [5 6] 2 3
fmt.Println(s2, len(s2), cap(s2))
s1 和 s2 可以用下圖表示:

s1只能訪問array的前三個(gè)元素,s2只能訪問5和6這兩個(gè)元素。s1的容量是 7(底層數(shù)組的長度)s2的容量是 3,從5所在的索引處直到底層數(shù)組的末尾。
對(duì)于 s1 和 s2,我們都沒有指定它的容量,但是我們打印發(fā)現(xiàn)它們都有容量, 其實(shí)在切片中,我們從切片中生成一個(gè)新的切片的時(shí)候,如果我們不指定容量, 那新切片的容量就是 s[start:end] 中的 start 直到底層數(shù)組的最后一個(gè)元素的長度。
切片可以共享底層數(shù)組
切片最需要注意的點(diǎn)是,當(dāng)我們從一個(gè)切片中創(chuàng)建新的切片的時(shí)候,兩者會(huì)共享同一個(gè)底層數(shù)組, 如上圖的那樣,s1 和 s2 都引用了同一個(gè)底層的數(shù)組不同的索引, s1 引用了底層數(shù)組的 0~2 下標(biāo)范圍,s2 引用了底層數(shù)組 4~5 下標(biāo)范圍。
這意味著,當(dāng)我們修改 s1 或 s2 的時(shí)候,原來的切片 a 也會(huì)發(fā)生改變:
var a = make([]int, 7, 7)
for i := 0; i < 7; i++ {
a[i] = i + 1
}
// [1 2 3 4 5 6 7]
fmt.Println(a)
s1 := a[:3]
// [1 2 3]
fmt.Println(s1)
s1[1] = 100
// [1 100 3 4 5 6 7]
fmt.Println(a)
// [1 100 3]
fmt.Println(s1)
在上面的例子中,s1 這個(gè)切片引用了和 a 一樣的底層數(shù)組, 然后在我們修改 s1 的時(shí)候,a 也發(fā)生了改變。
切片擴(kuò)容不會(huì)影響原切片
上一小節(jié)我們說了,切片可以共享底層數(shù)組。但是如果切片擴(kuò)容的話,那就是一個(gè)全新的切片了。
var a = []int{1, 2, 3}
// [1 2 3] 3 3
fmt.Println(a, len(a), cap(a))
// a 容納不下新的元素了,會(huì)進(jìn)行擴(kuò)容
b := append(a, 4)
// [1 2 3 4] 4 6
fmt.Println(b, len(b), cap(b))
b[1] = 100
// [1 2 3]
fmt.Println(a)
// [1 100 3 4]
fmt.Println(b)
在上面這個(gè)例子中,a 是一個(gè)長度和容量都是 3 的切片,這也就意味著,這個(gè)切片已經(jīng)滿了。 在這種情況下,我們再往其中追加元素的時(shí)候,就會(huì)進(jìn)行擴(kuò)容,生成一個(gè)新的切片。 因此,我們可以看到,我們修改了 b 的時(shí)候,并沒有影響到 a。
下面的例子就不一樣了:
// 長度為 2,容量為 3 var a = make([]int, 2, 3) a[0] = 1 a[1] = 2 // [1 2] 2 3 fmt.Println(a, len(a), cap(a)) // a 還可以容納新的元素,不用擴(kuò)容 b := append(a, 4) // [1 2 4] 3 3 fmt.Println(b, len(b), cap(b)) b[1] = 100 // [1 100] fmt.Println(a) // [1 100 4] fmt.Println(b)
在后面這個(gè)例子中,我們只是簡單地改了一下 a 初始化的方式,改成了只放入兩個(gè)元素,但是容量還是 3, 在這種情況下,a 可以再容納一個(gè)元素,這樣在 b := append(a, 4) 的時(shí)候,創(chuàng)建的 b 底層的數(shù)組其實(shí)跟 a 的底層數(shù)組依然是一樣的。
所以,我們需要尤其注意代碼中作為切片的函數(shù)參數(shù),如果我們希望在被調(diào)函數(shù)中修改了切片之后,在 caller 里面也能看到效果的話,最好是傳遞指針。
func test1(s []int) {
s = append(s, 4)
}
func test2(s *[]int) {
*s = append(*s, 4)
}
func TestSlice(t *testing.T) {
var a = []int{1, 2, 3}
// [1 2 3] 3 3
fmt.Println(a, len(a), cap(a))
test1(a)
// [1 2 3] 3 3
fmt.Println(a, len(a), cap(a))
var b = []int{1, 2, 3}
// [1 2 3] 3 3
fmt.Println(b, len(b), cap(b))
test2(&b)
// [1 2 3 4] 4 6
fmt.Println(b, len(b), cap(b))
}
在上面的例子中,test1 接收的是值參數(shù),所以在 test1 中切片發(fā)生擴(kuò)容的時(shí)候,TestSlice 里面的 a 還是沒有發(fā)生改變。 而 test2 接收的是指針參數(shù),所以在 test2 中發(fā)生切片擴(kuò)容的時(shí)候,TestSlice 里面的 b 也發(fā)生了改變。
總結(jié)
- 數(shù)組跟切片的使用上有點(diǎn)類似,但是數(shù)組代表的是有固定長度的數(shù)據(jù)序列,而切片代表的是沒有固定長度的數(shù)據(jù)序列。
- 數(shù)組的長度是類型的一部分,有兩種定義數(shù)組的方式:
[2]int{1, 2}、[...]int{1, 2}。 - 數(shù)組跟切片都可以通過下標(biāo)來訪問其中的元素,可以訪問的下標(biāo)范圍都是
0 ~ len(x)-1,x表示的是數(shù)組或者切片。 - 數(shù)組無法追加新的元素,切片可以追加任意數(shù)量的元素。
slice的數(shù)據(jù)結(jié)構(gòu)里面包含了:array底層數(shù)組指針、len切片長度、cap切片容量。- 創(chuàng)建切片的時(shí)候,指定一個(gè)合適的容量可以減少內(nèi)存分配的次數(shù),從而在一定程度上提高程序性能。
- 我們可以從數(shù)組或者切片創(chuàng)建一個(gè)新的切片:
array[1:3]或者slice[1:3]。 - 使用
append內(nèi)置函數(shù)可以往切片中添加新的元素。 - 使用
copy內(nèi)置函數(shù)可以將一個(gè)切片的內(nèi)容復(fù)制到另外一個(gè)切片中。 - 切片刪除元素沒有好的辦法,只能截取被刪除元素前后的數(shù)據(jù),然后復(fù)制到一個(gè)新的切片中。
- 假設(shè)我們通過
slice[start:end]的方式從切片中創(chuàng)建一個(gè)新的切片,那么這個(gè)新的切片的容量是cap(slice) - start,也就是,從start到底層數(shù)組最后一個(gè)元素的長度。 - 使用切片的時(shí)候需要注意:切片之間會(huì)共享底層數(shù)組,其中一個(gè)切片修改了切片的元素的時(shí)候,也會(huì)反映到其他切片上。
- 函數(shù)調(diào)用的時(shí)候,如果被調(diào)函數(shù)內(nèi)發(fā)生擴(kuò)容,調(diào)用者是無法知道的。如果我們不想錯(cuò)過在被調(diào)函數(shù)內(nèi)切片的變化,我們可以傳遞指針作為參數(shù)。
以上就是go slice 數(shù)組和切片使用區(qū)別示例解析的詳細(xì)內(nèi)容,更多關(guān)于go slice 數(shù)組切片區(qū)別的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Golang源碼分析之golang/sync之singleflight
golang/sync庫拓展了官方自帶的sync庫,提供了errgroup、semaphore、singleflight及syncmap四個(gè)包,本次先分析第一個(gè)包errgroup的源代碼,下面這篇文章主要給大家介紹了關(guān)于Golang源碼分析之golang/sync之singleflight的相關(guān)資料,需要的朋友可以參考下2022-11-11
Go語言CSP并發(fā)模型goroutine及channel底層實(shí)現(xiàn)原理
這篇文章主要為大家介紹了Go語言CSP并發(fā)模型goroutine?channel底層實(shí)現(xiàn)原理,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-05-05

