go slice 數(shù)組和切片使用區(qū)別示例解析
正文
slice
(切片)是 go 里面非常常用的一種數(shù)據(jù)結(jié)構(gòu),它代表了一個(gè)變長(zhǎng)的序列,序列中的每個(gè)元素都有相同的數(shù)據(jù)類型。 一個(gè) slice
類型一般寫作 []T
,其中 T
代表 slice
中元素的類型;slice
的語(yǔ)法和數(shù)組很像,但是 slice
沒有固定長(zhǎng)度。
數(shù)組和切片的區(qū)別
數(shù)組有確定的長(zhǎng)度,而切片的長(zhǎng)度不固定,并且可以自動(dòng)擴(kuò)容。
數(shù)組的定義
go 中定義數(shù)組的方式有如下兩種:
- 指定長(zhǎng)度:
arr := [3]int{1, 2, 3}
- 不指定長(zhǎng)度,由編譯器推導(dǎo)出數(shù)組的長(zhǎng)度:
arr := [...]{1, 2, 3}
上面這兩種定義方式都定義了一個(gè)長(zhǎng)度為 3 的數(shù)組。正如我們所見,長(zhǎng)度是數(shù)組的一部分,定義數(shù)組的時(shí)候長(zhǎng)度已經(jīng)確定下來(lái)了。
切片的定義
切片的定義方式跟數(shù)組很像,只不過(guò)定義切片的時(shí)候不用指定長(zhǎng)度:
s := []int{1, 2, 3}
在上面定義切片的代碼中,我們可以看到其實(shí)跟數(shù)組唯一的區(qū)別就是少了個(gè)長(zhǎng)度。 那其實(shí)我們可以把切片看作是一個(gè)無(wú)限長(zhǎng)度的數(shù)組。 當(dāng)然,實(shí)際上它并不是無(wú)限的,它只是在切片容納不下新的元素的時(shí)候,會(huì)自動(dòng)進(jìn)行擴(kuò)容,從而可以容納更多的元素。
數(shù)組和切片的相似之處
正如我們上面看到的那樣,數(shù)組和切片兩者其實(shí)非常相似,在實(shí)際使用中,它們也是有些類似的。
比如,通過(guò)下標(biāo)來(lái)訪問(wèn)元素:
arr := [3]int{1, 2, 3} // 通過(guò)下標(biāo)訪問(wèn) fmt.Println(arr[1]) // 2 s := []int{1, 2, 3} // 通過(guò)下標(biāo)訪問(wèn) fmt.Println(s[1]) // 2
數(shù)組的局限
我們知道了,數(shù)組的長(zhǎng)度是固定的,這也就意味著如果我們想往數(shù)組里面增加一個(gè)元素會(huì)比較麻煩, 我們需要新建一個(gè)更大的數(shù)組,然后將舊的數(shù)據(jù)復(fù)制過(guò)去,然后將新的元素寫進(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)
這樣一來(lái)就非常的繁瑣,如果我們使用切片,就可以省去這些步驟:
// 定義一個(gè)長(zhǎng)度為 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ù)組固定長(zhǎng)度的缺點(diǎn),實(shí)際使用中切片會(huì)使用得更加普遍。
重新理解 slice
在開始之前,我們來(lái)看看 slice
這個(gè)單詞的意思:作為名詞,slice
的意思有 片;部分;(切下的食物)薄片;,作為動(dòng)詞,slice
的意思有 切;把…切成(薄)片; 的意思。 從這個(gè)角度出發(fā),我們可以把 slice
理解為從某個(gè)數(shù)組上 切下來(lái)的一部分(從這個(gè)角度看,slice
這個(gè)命名非常的形象)。我們可以看看下圖:
在這個(gè)圖中,A
是一個(gè)保存了數(shù)字 1~7
的 slice
,B
是從 A
中 切下來(lái)的一部分,而 B
只包含了 A
中的一部分?jǐn)?shù)據(jù)。 我們可以把 B
理解為 A
的一個(gè) 視圖,B
中的數(shù)據(jù)是 A
中的數(shù)據(jù)的一個(gè) 引用,而不是 A
中數(shù)據(jù)的一個(gè) 拷貝 (也就是說(shuō),我們修改 B
的時(shí)候,A
中的數(shù)據(jù)也會(huì)被修改,當(dāng)然會(huì)有例外,那就是 B
發(fā)生擴(kuò)容的時(shí)候,再去修改 B
的話就影響不了 A
了)。
slice 的內(nèi)存布局
現(xiàn)在假設(shè)我們有如下代碼:
// 創(chuàng)建一個(gè)切片,長(zhǎng)度為 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)存布局如下:
說(shuō)明:
slice
底層其實(shí)也是數(shù)組,但是除了數(shù)組之外,還有兩個(gè)字段記錄切片的長(zhǎng)度和容量,分別是len
和cap
。- 上圖中,
slice
中的array
就是切片的底層數(shù)組,因?yàn)樗拈L(zhǎng)度不是固定的,所以使用了指針來(lái)保存,指向了另外一片內(nèi)存區(qū)域。 len
表明了切片的長(zhǎng)度,切片的長(zhǎng)度也就是我們可以操作的下標(biāo),上面的切片長(zhǎng)度為3
,這也就意味著我們切片可以操作的下標(biāo)范圍是0~2
。超出這個(gè)范圍的下標(biāo)會(huì)報(bào)錯(cuò)。cap
表明了切片的容量,也就是切片擴(kuò)容之前可以容納的元素個(gè)數(shù)。
切片容量存在的意義
對(duì)于我們?nèi)粘i_發(fā)來(lái)說(shuō),slice
的容量其實(shí)大多數(shù)時(shí)候不是我們需要關(guān)注的點(diǎn),而且由于容量的存在,也給開發(fā)者帶來(lái)了一定的困惑。 那么容量存在的意義是什么呢?意義就在于避免內(nèi)存的頻繁分配帶來(lái)的性能下降(容量也就是提前分配的內(nèi)存大小)。
比如,假如我們有一個(gè)切片,然后我們知道需要往它里面存放 1w 個(gè)元素, 如果我們不指定容量的話,那么切片就會(huì)在它存放不下新的元素的時(shí)候進(jìn)行擴(kuò)容, 這樣一來(lái),可能在我們存放這 1w 個(gè)元素的時(shí)候需要進(jìn)行多次擴(kuò)容, 這也就意味著需要進(jìn)行多次的內(nèi)存分配。這樣就會(huì)影響應(yīng)用的性能。
我們可以通過(guò)下面的例子來(lái)簡(jiǎn)單了解一下:
// 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]
說(shuō)明:
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è)元素處。
除此之外,我們還可以指定新的切片的容量,通過(guò)如下這種方式:
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
往切片中添加元素
我們前面說(shuō)過(guò)了,如果我們想往數(shù)組里面增加元素,那么我們必須開辟新的內(nèi)存,將舊的數(shù)組復(fù)制過(guò)去,然后才能將新的元素加入進(jìn)去。
但是切片就相對(duì)簡(jiǎn)單,我們可以使用 append
這個(gè)內(nèi)置函數(shù)來(lái)往切片中加入新的元素:
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
的長(zhǎng)度比 src
的長(zhǎng)度小,那么只會(huì)截取 src
的前面一部分。
從切片刪除元素
雖然我們往切片追加元素的操作挺方便的,但是要從切片刪除元素就相對(duì)麻煩一些了。go 語(yǔ)言本身沒有提供從切片刪除元素的方法。 如果我們要?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
只能訪問(wèn)array
的前三個(gè)元素,s2
只能訪問(wèn)5
和6
這兩個(gè)元素。s1
的容量是 7(底層數(shù)組的長(zhǎng)度)s2
的容量是 3,從5
所在的索引處直到底層數(shù)組的末尾。
對(duì)于 s1
和 s2
,我們都沒有指定它的容量,但是我們打印發(fā)現(xiàn)它們都有容量, 其實(shí)在切片中,我們從切片中生成一個(gè)新的切片的時(shí)候,如果我們不指定容量, 那新切片的容量就是 s[start:end]
中的 start
直到底層數(shù)組的最后一個(gè)元素的長(zhǎng)度。
切片可以共享底層數(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í)候,原來(lái)的切片 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é)我們說(shuō)了,切片可以共享底層數(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è)長(zhǎng)度和容量都是 3
的切片,這也就意味著,這個(gè)切片已經(jīng)滿了。 在這種情況下,我們?cè)偻渲凶芳釉氐臅r(shí)候,就會(huì)進(jìn)行擴(kuò)容,生成一個(gè)新的切片。 因此,我們可以看到,我們修改了 b
的時(shí)候,并沒有影響到 a
。
下面的例子就不一樣了:
// 長(zhǎng)度為 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è)例子中,我們只是簡(jiǎn)單地改了一下 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ù)組代表的是有固定長(zhǎng)度的數(shù)據(jù)序列,而切片代表的是沒有固定長(zhǎng)度的數(shù)據(jù)序列。
- 數(shù)組的長(zhǎng)度是類型的一部分,有兩種定義數(shù)組的方式:
[2]int{1, 2}
、[...]int{1, 2}
。 - 數(shù)組跟切片都可以通過(guò)下標(biāo)來(lái)訪問(wèn)其中的元素,可以訪問(wèn)的下標(biāo)范圍都是
0 ~ len(x)-1
,x
表示的是數(shù)組或者切片。 - 數(shù)組無(wú)法追加新的元素,切片可以追加任意數(shù)量的元素。
slice
的數(shù)據(jù)結(jié)構(gòu)里面包含了:array
底層數(shù)組指針、len
切片長(zhǎng)度、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è)我們通過(guò)
slice[start:end]
的方式從切片中創(chuàng)建一個(gè)新的切片,那么這個(gè)新的切片的容量是cap(slice) - start
,也就是,從start
到底層數(shù)組最后一個(gè)元素的長(zhǎng)度。 - 使用切片的時(shí)候需要注意:切片之間會(huì)共享底層數(shù)組,其中一個(gè)切片修改了切片的元素的時(shí)候,也會(huì)反映到其他切片上。
- 函數(shù)調(diào)用的時(shí)候,如果被調(diào)函數(shù)內(nèi)發(fā)生擴(kuò)容,調(diào)用者是無(wú)法知道的。如果我們不想錯(cuò)過(guò)在被調(diào)函數(shù)內(nèi)切片的變化,我們可以傳遞指針作為參數(shù)。
以上就是go slice 數(shù)組和切片使用區(qū)別示例解析的詳細(xì)內(nèi)容,更多關(guān)于go slice 數(shù)組切片區(qū)別的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Golang源碼分析之golang/sync之singleflight
golang/sync庫(kù)拓展了官方自帶的sync庫(kù),提供了errgroup、semaphore、singleflight及syncmap四個(gè)包,本次先分析第一個(gè)包errgroup的源代碼,下面這篇文章主要給大家介紹了關(guān)于Golang源碼分析之golang/sync之singleflight的相關(guān)資料,需要的朋友可以參考下2022-11-11Go語(yǔ)言學(xué)習(xí)筆記之文件讀寫操作詳解
這篇文章主要為大家詳細(xì)介紹了Go語(yǔ)言對(duì)文件進(jìn)行讀寫操作的方法,文中的示例代碼講解詳細(xì),對(duì)我們學(xué)習(xí)Go語(yǔ)言有一定的幫助,需要的可以參考一下2022-05-05Go語(yǔ)言基礎(chǔ)if條件語(yǔ)句用法及示例詳解
這篇文章主要為大家介紹了Go語(yǔ)言基礎(chǔ)if條件語(yǔ)句的用法及示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步早日升職加薪2021-11-11Go語(yǔ)言CSP并發(fā)模型goroutine及channel底層實(shí)現(xiàn)原理
這篇文章主要為大家介紹了Go語(yǔ)言CSP并發(fā)模型goroutine?channel底層實(shí)現(xiàn)原理,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-05-05