一、數(shù)組
《快學(xué) Go 語言》第 4 課 —— 低調(diào)的數(shù)組
Go 語言里面的數(shù)組其實(shí)很不常用裂垦,這是因?yàn)閿?shù)組是定長的靜態(tài)的顺囊,一旦定義好長度就無法更改,而且不同長度的數(shù)組屬于不同的類型蕉拢,之間不能相互轉(zhuǎn)換相互賦值特碳,用起來多有不方便之處。
切片是動(dòng)態(tài)的數(shù)組晕换,是可以擴(kuò)充內(nèi)容增加長度的數(shù)組午乓。當(dāng)長度不變時(shí),它用起來就和普通數(shù)組一樣闸准。當(dāng)長度不同時(shí)益愈,它們也屬于相同的類型,之間可以相互賦值夷家。這就決定了數(shù)組的應(yīng)用領(lǐng)域都廣泛地被切片取代了蒸其。
1.只聲明的話,全部是零值
func main() {
var a [9]int
fmt.Println(a)
}
------------
[0 0 0 0 0 0 0 0 0]
三種聲明方式,給初值方式是一樣的
var a = [9]int{1, 2, 3, 4, 5, 6, 7, 8, 9}
var b [10]int = [10]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
c := [8]int{1, 2, 3, 4, 5, 6, 7, 8}
//[0,10,20,0,0]
array := [5]int{1:10,2:20}
2.下標(biāo)訪問
func main() {
var squares [9]int
for i := 0; i < len(squares); i++ {
squares[i] = (i + 1) * (i + 1)
}
fmt.Println(squares)
}
--------------------
[1 4 9 16 25 36 49 64 81]
3.數(shù)組賦值
同樣的子元素類型并且是同樣長度的數(shù)組才可以相互賦值库快,否則就是不同的數(shù)組類型摸袁,不能賦值。數(shù)組的賦值本質(zhì)上是一種淺拷貝操作缺谴,賦值的兩個(gè)數(shù)組變量的值不會(huì)共享但惶。
func main() {
var a = [9]int{1, 2, 3, 4, 5, 6, 7, 8, 9}
var b [9]int
b = a
a[0] = 12345
fmt.Println(a)
fmt.Println(b)
}
--------------------------
[12345 2 3 4 5 6 7 8 9]
[1 2 3 4 5 6 7 8 9]
4.range關(guān)鍵字來遍歷
func main() {
var a = [5]int{1,2,3,4,5}
for index := range a {
fmt.Println(index, a[index])
}
for index, value := range a {
fmt.Println(index, value)
}
}
------------
0 1
1 2
2 3
3 4
4 5
0 1
1 2
2 3
3 4
4 5
每次循環(huán)迭代, range 產(chǎn)生一對(duì)值湿蛔;索引以及在該索引處的元素值膀曾。如果不需要索引怎么辦,range 的語法要求, 要處理元素, 必須處理索引阳啥。一種思路是把索引賦值給一個(gè)臨時(shí)變量,如 temp , 然后忽略它的值添谊,但Go語言不允許使用無用的局部變量(local variables),因?yàn)檫@會(huì)導(dǎo)致編譯錯(cuò)誤察迟。Go語言中這種情況的解決方法是用 空標(biāo)識(shí)符 (blank identifier)斩狱,即 _ (也就是下劃線)耳高。空標(biāo)識(shí)符可用于任何語法需要變量名但程序邏輯不需要的時(shí)候, 例如, 在循環(huán)里所踊,丟棄不需要的循環(huán)索引, 保留元素值泌枪。
for _,value := range s1{
fmt.Println(value)
}
注意:range創(chuàng)建了每個(gè)元素的副本,而不是直接返回對(duì)該元素的引用秕岛。range總是會(huì)從切片頭部開始迭代碌燕。
5.函數(shù)間傳遞數(shù)組
在函數(shù)間傳遞變量時(shí),總是以值的方式傳遞继薛。如果變量是個(gè)數(shù)組修壕,意味著整個(gè)數(shù)組,不管有多長遏考,都會(huì)完整復(fù)制慈鸠,并傳遞給函數(shù)。在這方面灌具,go語言對(duì)待數(shù)組的方式和其它很多編程語言不同青团,其它語言可能會(huì)隱式地將數(shù)組作為引用或指針對(duì)象傳入被調(diào)用的函數(shù)。
有一種更好且更有效的方法來處理這個(gè)操作稽亏,就是只傳入指向數(shù)組的指針壶冒,只需要復(fù)制8個(gè)字節(jié)的數(shù)據(jù),這樣危險(xiǎn)在于截歉,共享了內(nèi)存胖腾,會(huì)改變?cè)贾怠?/p>
二、切片
上圖中一個(gè)切片變量包含三個(gè)域瘪松,分別是底層數(shù)組的指針咸作、切片的長度 length 和切片的容量 capacity。切片支持 append 操作可以將新的內(nèi)容追加到底層數(shù)組宵睦,也就是填充上面的灰色格子记罚。如果格子滿了,切片就需要擴(kuò)容壳嚎,底層的數(shù)組就會(huì)更換桐智。
形象一點(diǎn)說,切片變量是底層數(shù)組的視圖烟馅,底層數(shù)組是臥室说庭,切片變量是臥室的窗戶。通過窗戶我們可以看見底層數(shù)組的一部分或全部郑趁。一個(gè)臥室可以有多個(gè)窗戶刊驴,不同的窗戶能看到臥室的不同部分。
1.切片的創(chuàng)建有多種方式,我們先看切片最通用的創(chuàng)建方法捆憎,那就是內(nèi)置的 make 函數(shù)
var s1 []int = make([]int, 5, 8)
var s2 []int = make([]int, 8) // 滿容切片
make 函數(shù)創(chuàng)建切片舅柜,需要提供三個(gè)參數(shù),分別是切片的類型躲惰、切片的長度和容量致份。其中第三個(gè)參數(shù)是可選的,如果不提供第三個(gè)參數(shù)础拨,那么長度和容量相等知举,也就是說切片的滿容的。
使用 make 函數(shù)創(chuàng)建的切片內(nèi)容是「零值切片」太伊,也就是內(nèi)部數(shù)組的元素都是零值。Go 語言還提供了另一個(gè)種創(chuàng)建切片的語法逛钻,允許我們給它賦初值僚焦。使用這種方式創(chuàng)建的切片是滿容的。
func main() {
var s []int = []int{1,2,3,4,5} // 滿容的
fmt.Println(s, len(s), cap(s))
}
---------
[1 2 3 4 5] 5 5
這種寫法曙痘,和數(shù)組的定義非常相似芳悲,注意區(qū)別就在方括號(hào)里是否寫了長度。
var i1 [5]int = [5]int{1,2,3,4,5}
var i2 []int = []int{1,2,3,4,5}
i1 = append(i1, 6)
i2 = append(i2,7)
編譯不通過边坤,i1用不了append方法名扛,參數(shù)必須是slice
2.切片的賦值
切片的賦值是一次淺拷貝操作,拷貝的是切片變量的三個(gè)域茧痒,你可以將切片變量看成長度為 3 的 int 型數(shù)組肮韧,數(shù)組的賦值就是淺拷貝⊥拷貝前后兩個(gè)變量共享底層數(shù)組弄企,對(duì)一個(gè)切片的修改會(huì)影響另一個(gè)切片的內(nèi)容,這點(diǎn)需要特別注意区拳。
func main() {
var s1 = make([]int, 5, 8)
// 切片的訪問和數(shù)組差不多
for i := 0; i < len(s1); i++ {
s1[i] = i + 1
}
var s2 = s1
fmt.Println(s1, len(s1), cap(s1))
fmt.Println(s2, len(s2), cap(s2))
// 嘗試修改切片內(nèi)容
s2[0] = 255
fmt.Println(s1)
fmt.Println(s2)
}
--------
[1 2 3 4 5] 5 8
[1 2 3 4 5] 5 8
[255 2 3 4 5]
[255 2 3 4 5]
3.append追加
相對(duì)于數(shù)組拘领,切片可以增加長度。當(dāng)append返回時(shí)樱调,會(huì)返回一個(gè)包含修改結(jié)果的新切片约素。函數(shù)append總會(huì)增加新切片的長度,而容量有可能改變笆凌,也有可能不改變圣猎,這取決于被操作切片的可用容量。
slice := []int{10,20,30,40,50}
newSlice := slice[1:3]
newSlice = append(newSlice,60);
因?yàn)閚ewSlice在底層數(shù)組里還有額外容量可用菩颖,append操作將可用元素合并到切片的長度样漆,并對(duì)其進(jìn)行賦值。由于和原始的slice共享同一個(gè)底層數(shù)組晦闰,slice中索引為3的元素值也被改動(dòng)了放祟。
如果切片的底層數(shù)組沒有足夠可用容量鳍怨,append函數(shù)會(huì)創(chuàng)建一個(gè)新的底層數(shù)組,將被引用的現(xiàn)有值復(fù)制到新數(shù)組里跪妥,再追加新值鞋喇。
slice := []int{10,20,30,40}
newSlice = append(slice,50)
函數(shù)append會(huì)自動(dòng)處理底層數(shù)組的容量增長,在切片容量小于1000個(gè)元素時(shí)眉撵,總是會(huì)成倍地增加容量侦香。超過1000時(shí),每次增加25%纽疟。
append可以在一次調(diào)用傳遞多個(gè)追加值罐韩,如果使用...運(yùn)算符,可以將一個(gè)切片所有元素追加到另一個(gè)切片里
s1 := []int{1,2}
s2 := []int{3,4}
append(s1,s2...);
4.切割
func main() {
var s1 = []int{1,2,3,4,5,6,7}
// start_index 和 end_index污朽,不包含 end_index
// [start_index, end_index)
var s2 = s1[2:5]
s2[0] = 0
fmt.Println(s1, len(s1), cap(s1))
fmt.Println(s2, len(s2), cap(s2))
}
------------
[1 2 0 4 5 6 7] 7 7
[0 4 5] 3 5
我們注意到子切片的內(nèi)部數(shù)據(jù)指針指向了數(shù)組的中間位置散吵,而不再是數(shù)組的開頭了。子切片容量的大小是從中間的位置開始直到切片末尾的長度蟆肆,母子切片依舊共享底層數(shù)組矾睦。
子切片語法上要提供起始和結(jié)束位置,這兩個(gè)位置都可選的炎功,不提供起始位置枚冗,默認(rèn)就是從母切片的初始位置開始(不是底層數(shù)組的初始位置),不提供結(jié)束位置蛇损,默認(rèn)就結(jié)束到母切片尾部(是長度線赁温,不是容量線)。使用過 Python 的同學(xué)可能會(huì)問州藕,切片支持負(fù)數(shù)的位置么束世,答案是不支持,下標(biāo)不可以是負(fù)數(shù)床玻。
對(duì)數(shù)組進(jìn)行切割可以轉(zhuǎn)換成切片毁涉,切片將原數(shù)組作為內(nèi)部底層數(shù)組。也就是說修改了原數(shù)組會(huì)影響到新切片锈死,對(duì)切片的修改也會(huì)影響到原數(shù)組贫堰。
func main() {
var a = [10]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
var b = a[2:6]
fmt.Println(b)
a[4] = 100
fmt.Println(b)
}
-------
[3 4 5 6]
[3 4 100 6]
5.切割的第三個(gè)參數(shù)
source := []string{"apple","orange","plum","banana","grape"};
slice := source[2:3]//plum1個(gè)元素,到結(jié)尾待牵,3個(gè)容量
slice := source[2:3:4]//也是一個(gè)1元素其屏,不過有4-2=2個(gè)容量
使用第3個(gè)參數(shù)將長度和容量保持一致后,再使用append操作就會(huì)創(chuàng)建新的底層數(shù)組缨该,從而和原底層數(shù)組分離偎行,這樣就不用擔(dān)心影響到其他切片中的數(shù)據(jù)。
6.內(nèi)置 copy 函數(shù) func copy(dst, src []T) int
copy 函數(shù)不會(huì)因?yàn)樵衅湍繕?biāo)切片的長度問題而額外分配底層數(shù)組的內(nèi)存,它只負(fù)責(zé)拷貝數(shù)組的內(nèi)容蛤袒,從原切片拷貝到目標(biāo)切片熄云,拷貝的量是原切片和目標(biāo)切片長度的較小值 —— min(len(src), len(dst)),函數(shù)返回的是拷貝的實(shí)際長度妙真。
func main() {
var s = make([]int, 5, 8)
for i:=0;i<len(s);i++ {
s[i] = i+1
}
fmt.Println(s)
var d = make([]int, 2, 6)
var n = copy(d, s)
fmt.Println(n, d)
}
-----------
[1 2 3 4 5]
2 [1 2]
7.range遍歷
需要強(qiáng)調(diào)的是缴允,range創(chuàng)建了每個(gè)元素的副本,而不是直接返回該元素的引用
// 創(chuàng)建一個(gè)整型切片
// 其長度和容量都是 4 個(gè)元素
slice := []int{10, 20, 30, 40}
// 迭代每個(gè)元素珍德,并顯示值和地址
for index, value := range slice {
fmt.Printf("Value: %d Value-Addr: %X ElemAddr: %X\n",
value, &value, &slice[index])
}
Output:
Value: 10 Value-Addr: 10500168 ElemAddr: 1052E100
Value: 20 Value-Addr: 10500168 ElemAddr: 1052E104
Value: 30 Value-Addr: 10500168 ElemAddr: 1052E108
Value: 40 Value-Addr: 10500168 ElemAddr: 1052E10C
因?yàn)榈祷氐淖兞渴且粋€(gè)迭代過程中根據(jù)切片依次賦值的新變量练般,所以 value 的地址總是相同的。要想獲取每個(gè)元素的地址锈候,可以使用切片變量和索引值薄料。
8.沒有push,pop這些功能
參考[譯]Go Slice 秘籍
Pop
x, a = a[len(a)-1], a[:len(a)-1]
Push
a = append(a, x)
Shift
x, a := a[0], a[1:]
Unshift
a = append([]T{x}, a...)
要?jiǎng)h除slice中間的某個(gè)元素并保存原有的元素順序,可以通過內(nèi)置的copy函數(shù)將后面的子slice向前依次移動(dòng)一位完成
func remove(slice []int, i int) []int {
copy(slice[i:], slice[i+1:])
return slice[:len(slice)-1]
}
參照上面的copy解釋泵琳,slice[i+1:]的長度必定是小于slice[i:]的都办,所以不會(huì)出現(xiàn)copy過程中丟失自己想留的數(shù)據(jù)。然后copy不改變?cè)械讓訑?shù)組的len和cap虑稼,只是把數(shù)據(jù)往前覆蓋了一個(gè)元素,所以必須使用return slice[:len(slice)-1]才是想要的結(jié)果势木。
9.可變參數(shù)
func sum(vals...int) int {
total := 0
for _, val := range vals {
total += val
}
return total
}
sum函數(shù)返回任意個(gè)int型參數(shù)的和蛛倦。在函數(shù)體中,vals被看作是類型為[] int的切片。sum可以接收任意數(shù)量的int型參數(shù):
fmt.Println(sum()) // "0"
fmt.Println(sum(3)) // "3"
fmt.Println(sum(1, 2, 3, 4)) // "10"
10.清空
參考 展示不同方式清空 slice 的效果
package main
import (
"fmt"
)
func dump(letters []string) {
fmt.Printf("addr = %p\n", letters)
fmt.Println("letters = ", letters)
fmt.Println(cap(letters))
fmt.Println(len(letters))
for i := range letters {
fmt.Println(i, letters[i])
}
}
func main() {
fmt.Println("=== 基礎(chǔ)數(shù)據(jù) ==========")
letters := []string{"a", "b", "c", "d"}
dump(letters)
fmt.Println("=== ====== ==========")
fmt.Println("=== \"原地\"清空 ===")
fmt.Println("=== 效果:")
fmt.Println("=== 1.直接在原 slice 上操作啦桌,故無 GC 行為")
fmt.Println("=== 2.清空后 cap 值和之前相同溯壶,len 值清零")
letters = letters[:0]
dump(letters)
fmt.Println("=== 添加元素效果:基于原 slice 操作,故再未超 cap 前無需內(nèi)存分配")
letters = append(letters, "e")
dump(letters)
fmt.Println("=== ====== ==========")
fmt.Println("=== 基于 nil 清空 ===")
fmt.Println("=== 效果:")
fmt.Println("=== 1.類似 C 語言中賦值空指針甫男,原內(nèi)容會(huì)被 GC 處理")
fmt.Println("=== 2.清空后 cap 值清零且改,len 值清零")
letters = nil
dump(letters)
fmt.Println("=== 添加元素效果:類似從無到有創(chuàng)建 slice")
letters = append(letters, "e")
dump(letters)
}
運(yùn)行結(jié)果
=== 基礎(chǔ)數(shù)據(jù) ==========
addr = 0xc420070080
letters = [a b c d]
4
4
0 a
1 b
2 c
3 d
=== ====== ==========
=== "原地"清空 ===
=== 效果:
=== 1.直接在原 slice 上操作,故無 GC 行為
=== 2.清空后 cap 值和之前相同板驳,len 值清零
addr = 0xc420070080
letters = []
4
0
=== 添加元素效果:基于原 slice 操作又跛,故再未超 cap 前無需內(nèi)存分配
addr = 0xc420070080
letters = [e]
4
1
0 e
=== ====== ==========
=== 基于 nil 清空 ===
=== 效果:
=== 1.類似 C 語言中賦值空指針,原內(nèi)容會(huì)被 GC 處理
=== 2.清空后 cap 值清零若治,len 值清零
addr = 0x0
letters = []
0
0
=== 添加元素效果:類似從無到有創(chuàng)建 slice
addr = 0xc42007c270
letters = [e]
1
1
0 e