在嚴(yán)格意義上,Go中有三種一等公民容器類型:數(shù)組、切片和映射。 有時(shí)候,我們可以認(rèn)為字符串類型和通道類型也屬于容器類型。 但是,此篇文章只談及數(shù)組、切片和映射類型。
Go中有很多和容器類型相關(guān)的細(xì)節(jié),本文將逐一列出這些細(xì)節(jié)。
每個(gè)容器(值)用來表示和存儲(chǔ)一個(gè)元素(element)序列或集合。一個(gè)容器中的所有元素的類型是相同的。此相同的類型稱為此容器的類型的元素類型(或簡(jiǎn)稱此容器的元素類型)。
存儲(chǔ)在一個(gè)容器中的每個(gè)元素值都關(guān)聯(lián)著一個(gè)鍵值(key)。每個(gè)元素可以通過它的鍵值而被訪問到。 一個(gè)映射類型的鍵值類型必須為一個(gè)可比較類型。 數(shù)組和切片類型的鍵值類型均為內(nèi)置類型int
。 一個(gè)數(shù)組或切片的一個(gè)元素對(duì)應(yīng)的鍵值總是一個(gè)非負(fù)整數(shù)下標(biāo),此非負(fù)整數(shù)表示該元素在該數(shù)組或切片所有元素中的順序位置。此非負(fù)整數(shù)下標(biāo)亦常稱為一個(gè)元素索引(index)。
每個(gè)容器值有一個(gè)長(zhǎng)度屬性,用來表明此容器中當(dāng)前存儲(chǔ)了多少個(gè)元素。 一個(gè)數(shù)組或切片中的每個(gè)元素所關(guān)聯(lián)的非負(fù)整數(shù)索引鍵值的合法取值范圍為左閉右開區(qū)間[0, 此數(shù)組或切片的長(zhǎng)度)
。 一個(gè)映射值類型的容器值中的元素關(guān)聯(lián)的鍵值可以是任何此映射類型的鍵值類型的任何值。
這三種容器類型的值在使用上有很多的差別。這些差別多源于它們的內(nèi)存結(jié)構(gòu)的差異。 通過上一篇文章值部,我們得知每個(gè)數(shù)組值僅由一個(gè)直接部分組成,而一個(gè)切片或者映射值是由一個(gè)直接部分和一個(gè)可能的被此直接部分引用著的間接部分組成。
一個(gè)數(shù)組或者切片的所有元素緊挨著存放在一塊連續(xù)的內(nèi)存中。一個(gè)數(shù)組中的所有元素均存放在此數(shù)組值的直接部分,一個(gè)切片中的所有元素均存放在此切片值的間接部分。 在官方標(biāo)準(zhǔn)編譯器和運(yùn)行時(shí)中,映射是使用哈希表算法來實(shí)現(xiàn)的。所以一個(gè)映射中的所有元素也均存放在一塊連續(xù)的內(nèi)存中,但是映射中的元素并不一定緊挨著存放。 另外一種常用的映射實(shí)現(xiàn)算法是二叉樹算法。無論使用何種算法,一個(gè)映射中的所有元素的鍵值也存放在此映射值(的間接部分)中。
我們可以通過一個(gè)元素的鍵值來訪問此元素。 對(duì)于這三種容器,元素訪問的時(shí)間復(fù)雜度均為O(1)
。 但是一般來說,映射元素訪問消耗的時(shí)長(zhǎng)要數(shù)倍于數(shù)組和切片元素訪問消耗的時(shí)長(zhǎng)。 但是映射相對(duì)于數(shù)組和切片有兩個(gè)優(yōu)點(diǎn):
從上一篇文章中,我們已經(jīng)了解到,在任何賦值中,源值的底層間接部分不會(huì)被復(fù)制。 換句話說,當(dāng)一個(gè)賦值結(jié)束后,一個(gè)含有間接部分的源值和目標(biāo)值將共享底層間接部分。 這就是數(shù)組和切片/映射值會(huì)有很多行為差異(將在下面逐一介紹)的原因。
無名容器類型的字面表示形式如下:
[N]T
[]T
map[K]T
其中,
T
可為任意類型。它表示一個(gè)容器類型的元素類型。某個(gè)特定容器類型的值中只能存儲(chǔ)此容器類型的元素類型的值。N
必須為一個(gè)非負(fù)整數(shù)常量。它指定了一個(gè)數(shù)組類型的長(zhǎng)度,或者說它指定了此數(shù)組類型的任何一個(gè)值中存儲(chǔ)了多少個(gè)元素。 一個(gè)數(shù)組類型的長(zhǎng)度是此數(shù)組類型的一部分。比如[5]int
和[8]int
是兩個(gè)不同的類型。K
必須為一個(gè)可比較類型。它指定了一個(gè)映射類型的鍵值類型。下面列出了一些無名容器類型的字面表示:
const Size = 32
type Person struct {
name string
age int
}
// 數(shù)組類型
[5]string
[Size]int
[16][]byte // 元素類型為一個(gè)切片類型:[]byte
[100]Person // 元素類型為一個(gè)結(jié)構(gòu)體類型:Person
// 切片類型
[]bool
[]int64
[]map[int]bool // 元素類型為一個(gè)映射類型:map[int]bool
[]*int // 元素類型為一個(gè)指針類型:*int
// 映射類型
map[string]int
map[int]bool
map[int16][6]string // 元素類型為一個(gè)數(shù)組類型:[6]string
map[bool][]string // 元素類型為一個(gè)切片類型:[]string
map[struct{x int}]*int8 // 元素類型為一個(gè)指針類型:*int8;
// 鍵值類型為一個(gè)結(jié)構(gòu)體類型。
所有切片類型的尺寸都是一致的,所有映射類型的尺寸也都是一致的。 一個(gè)數(shù)組類型的尺寸等于它的元素類型的尺寸和它的長(zhǎng)度的乘積。長(zhǎng)度為零的數(shù)組的尺寸為零;元素類型尺寸為零的任意長(zhǎng)度的數(shù)組類型的尺寸也為零。
和結(jié)構(gòu)體值類似,容器值的文字表示也可以用組合字面量形式(composite literal)來表示。 比如對(duì)于一個(gè)容器類型T
,它的值可以用形式T{...}
來表示(除了切片和映射的零值外)。 下面是一些容器字面量:
// 一個(gè)含有4個(gè)布爾元素的數(shù)組值。
[4]bool{false, true, true, false}
// 一個(gè)含有三個(gè)字符串值的切片值。
[]string{"break", "continue", "fallthrough"}
// 一個(gè)映射值。
map[string]int{"C": 1972, "Python": 1991, "Go": 2009}
映射組合字面量中大括號(hào)中的每一項(xiàng)稱為一個(gè)鍵值對(duì)(key-value pair),或者稱為一個(gè)條目(entry)。
數(shù)組和切片組合字面量有一些微小的變種:
// 下面這些切片字面量都是等價(jià)的。
[]string{"break", "continue", "fallthrough"}
[]string{0: "break", 1: "continue", 2: "fallthrough"}
[]string{2: "fallthrough", 1: "continue", 0: "break"}
[]string{2: "fallthrough", 0: "break", "continue"}
// 下面這些數(shù)組字面量都是等價(jià)的。
[4]bool{false, true, true, false}
[4]bool{0: false, 1: true, 2: true, 3: false}
[4]bool{1: true, true}
[4]bool{2: true, 1: true}
[...]bool{false, true, true, false}
[...]bool{3: false, 1: true, true}
上例中最后兩行中的...
表示讓編譯器推斷出相應(yīng)數(shù)組值的類型的長(zhǎng)度。
從上面的例子中,我們可以看出數(shù)組和切片組合字面量中的索引下標(biāo)(即數(shù)組和切片的鍵值)是可選的。 在一個(gè)數(shù)組或者切片組合字面量中:
int
,但它必須是一個(gè)可以表示為int值的非負(fù)常量; 如果它是一個(gè)類型確定值,則它的類型必須為一個(gè)基本整數(shù)類型。映射組合字面量中元素對(duì)應(yīng)的鍵值不可缺失,并且它們可以為非常量。
var a uint = 1
var _ = map[uint]int {a : 123} // 沒問題
var _ = []int{a: 100} // error: 下標(biāo)必須為常量
var _ = [5]int{a: 100} // error: 下標(biāo)必須為常量
一個(gè)容器組合字面量中的常量鍵值(包括索引下標(biāo))不可重復(fù)。
和結(jié)構(gòu)體類似,一個(gè)數(shù)組類型A
的零值可以表示為A{}
。 比如,數(shù)組類型[100]int
的零值可以表示為[100]int{}
。 一個(gè)數(shù)組零值中的所有元素均為對(duì)應(yīng)數(shù)組元素類型的零值。
和指針一樣,所有切片和映射類型的零值均用預(yù)聲明的標(biāo)識(shí)符nil
來表示。
順便說一句,除了剛提到的三種類型,以后將介紹的函數(shù)、通道和接口類型的零值也用預(yù)聲明的標(biāo)識(shí)符nil
來表示。
在運(yùn)行時(shí)刻,即使一個(gè)數(shù)組變量在聲明的時(shí)候未指定初始值,它的元素所占的內(nèi)存空間也已經(jīng)被開辟出來。 但是一個(gè)nil切片或者映射值的元素的內(nèi)存空間尚未被開辟出來。
注意:[]T{}
表示類型[]T
的一個(gè)空切片值,它和[]T(nil)
是不等價(jià)的。 同樣,map[K]T{}
和map[K]T(nil)
也是不等價(jià)的。
我們已經(jīng)了解到結(jié)構(gòu)體(組合)字面量是不可尋址的但卻是可以被取地址的。 容器字面量也不例外。
一個(gè)例子:
package main
import "fmt"
func main() {
pm := &map[string]int{"C": 1972, "Go": 2009}
ps := &[]string{"break", "continue"}
pa := &[...]bool{false, true, true, false}
fmt.Printf("%T\n", pm) // *map[string]int
fmt.Printf("%T\n", ps) // *[]string
fmt.Printf("%T\n", pa) // *[4]bool
}
在某些情形下,內(nèi)嵌在其它組合字面量中的組合字面量可以簡(jiǎn)化為{...}
(即類型部分被省略掉了)。 內(nèi)嵌組合字面量前的取地址操作符&
有時(shí)也可以被省略。
比如,下面的組合字面量
// heads為一個(gè)切片值。它的類型的元素類型為*[4]byte。
// 此元素類型為一個(gè)基類型為[4]byte的指針類型。
// 此指針基類型為一個(gè)元素類型為byte的數(shù)組類型。
var heads = []*[4]byte{
&[4]byte{'P', 'N', 'G', ' '},
&[4]byte{'G', 'I', 'F', ' '},
&[4]byte{'J', 'P', 'E', 'G'},
}
可以被簡(jiǎn)化為
var heads = []*[4]byte{
{'P', 'N', 'G', ' '},
{'G', 'I', 'F', ' '},
{'J', 'P', 'E', 'G'},
}
下面這個(gè)數(shù)組組合字面量
type language struct {
name string
year int
}
var _ = [...]language{
language{"C", 1972},
language{"Python", 1991},
language{"Go", 2009},
}
可以被簡(jiǎn)化為
var _ = [...]language{
{"C", 1972},
{"Python", 1991},
{"Go", 2009},
}
下面這個(gè)映射組合字面量
type LangCategory struct {
dynamic bool
strong bool
}
// 此映射值的類型的鍵值類型為一個(gè)結(jié)構(gòu)體類型,
// 元素類型為另一個(gè)映射類型:map[string]int。
var _ = map[LangCategory]map[string]int{
LangCategory{true, true}: map[string]int{
"Python": 1991,
"Erlang": 1986,
},
LangCategory{true, false}: map[string]int{
"JavaScript": 1995,
},
LangCategory{false, true}: map[string]int{
"Go": 2009,
"Rust": 2010,
},
LangCategory{false, false}: map[string]int{
"C": 1972,
},
}
可以被簡(jiǎn)化為
var _ = map[LangCategory]map[string]int{
{true, true}: {
"Python": 1991,
"Erlang": 1986,
},
{true, false}: {
"JavaScript": 1995,
},
{false, true}: {
"Go": 2009,
"Rust": 2010,
},
{false, false}: {
"C": 1972,
},
}
注意,在上面的幾個(gè)例子中,最后一個(gè)元素后的逗號(hào)不能被省略。原因詳見后面的斷行規(guī)則一文。
在Go類型系統(tǒng)概述一文中,我們已經(jīng)了解到映射和切片類型都屬于不可比較類型。 所以任意兩個(gè)映射值(或切片值)是不能相互比較的。
盡管兩個(gè)映射值和切片值是不能比較的,但是一個(gè)映射值或者切片值可以和預(yù)聲明的nil
標(biāo)識(shí)符進(jìn)行比較以檢查此映射值或者切片值是否為一個(gè)零值。
大多數(shù)數(shù)組類型都是可比較類型,除了元素類型為不可比較類型的數(shù)組類型。
當(dāng)比較兩個(gè)數(shù)組值時(shí),它們的對(duì)應(yīng)元素將按照逐一被比較(可以認(rèn)為按照下標(biāo)順序比較)。這兩個(gè)數(shù)組只有在它們的對(duì)應(yīng)元素都相等的情況下才相等;當(dāng)一對(duì)元素被發(fā)現(xiàn)不相等的或者在比較中產(chǎn)生恐慌的時(shí)候,對(duì)數(shù)組的比較將提前結(jié)束。
一個(gè)例子:
package main
import "fmt"
func main() {
var a [16]byte
var s []int
var m map[string]int
fmt.Println(a == a) // true
fmt.Println(m == nil) // true
fmt.Println(s == nil) // true
fmt.Println(nil == map[string]int{}) // false
fmt.Println(nil == []int{}) // false
// 下面這些行編譯不通過。
/*
_ = m == m
_ = s == s
_ = m == map[string]int(nil)
_ = s == []int(nil)
var x [16][]int
_ = x == x
var y [16]map[int]bool
_ = y == y
*/
}
除了上面已提到的容器長(zhǎng)度屬性(此容器中含有有多少個(gè)元素),每個(gè)容器值還有一個(gè)容量屬性。 一個(gè)數(shù)組值的容量總是和它的長(zhǎng)度相等;一個(gè)非零映射值的容量可以被認(rèn)為是無限大的。切片值的容量的含義將在后續(xù)章節(jié)介紹。 一個(gè)切片值的容量總是不小于此切片值的長(zhǎng)度。在編程中,只有切片值的容量有實(shí)際意義。
我們可以調(diào)用內(nèi)置函數(shù)len
來獲取一個(gè)容器值的長(zhǎng)度,或者調(diào)用內(nèi)置函數(shù)cap
來獲取一個(gè)容器值的容量。 這兩個(gè)函數(shù)都返回一個(gè)int
類型確定結(jié)果值或者一個(gè)默認(rèn)類型為int
的類型不確定結(jié)果,具體取決于傳遞給它們的實(shí)參是否為常量表達(dá)式。 因?yàn)榉橇阌成渲档娜萘渴菬o限大,所以cap
并不適用于映射值。
一個(gè)數(shù)組值的長(zhǎng)度和容量永不改變。同一個(gè)數(shù)組類型的所有值的長(zhǎng)度和容量都總是和此數(shù)組類型的長(zhǎng)度相等。 切片值的長(zhǎng)度和容量可在運(yùn)行時(shí)刻改變(一般只能通過被賦值的途徑來修改,兩者一般不可單獨(dú)被修改)。 因?yàn)榇嗽?,切片可以被認(rèn)為是動(dòng)態(tài)數(shù)組。 切片在使用上相比數(shù)組更為靈活,所以切片(相對(duì)數(shù)組)在編程用得更為廣泛。
一個(gè)例子:
package main
import "fmt"
func main() {
var a [5]int
fmt.Println(len(a), cap(a)) // 5 5
var s []int
fmt.Println(len(s), cap(s)) // 0 0
s, s2 := []int{2, 3, 5}, []bool{}
fmt.Println(len(s), cap(s), len(s2), cap(s2)) // 3 3 0 0
var m map[int]bool
fmt.Println(len(m)) // 0
m, m2 := map[int]bool{1: true, 0: false}, map[int]int{}
fmt.Println(len(m), len(m2)) // 2 0
}
上面這個(gè)特定的例子中的每個(gè)切片值的長(zhǎng)度和容量都相等,但這并不是一個(gè)普遍定律。 我們將在后面的章節(jié)中展示一些長(zhǎng)度和容量不相等的切片值。
一個(gè)容器值v
中存儲(chǔ)的對(duì)應(yīng)著鍵值k
的元素用語法形式v[k]
來表示。 今后我們稱v[k]
為一個(gè)元素索引表達(dá)式。
假設(shè)v
是一個(gè)數(shù)組或者切片,在v[k]
中,
k
是一個(gè)常量,則它必須滿足上面列出的對(duì)出現(xiàn)在組合字面量中的索引的要求。 另外,如果v
是一個(gè)數(shù)組,則k
必須小于此數(shù)組的長(zhǎng)度。k
不是一個(gè)常量,則它必須為一個(gè)整數(shù)。 另外它必須為一個(gè)非負(fù)數(shù)并且小于len(v)
,否則,在運(yùn)行時(shí)刻將產(chǎn)生一個(gè)恐慌。v
是一個(gè)零值切片,則在運(yùn)行時(shí)刻將產(chǎn)生一個(gè)恐慌。假設(shè)v
是一個(gè)映射值,在v[k]
中,k
的類型必須為(或者可以隱式轉(zhuǎn)換為)v
的類型的元素類型。
另外,
k
是一個(gè)動(dòng)態(tài)類型為不可比較類型的接口值,則v[k]
在運(yùn)行時(shí)刻將造成一個(gè)恐慌;v[k]
被用做一個(gè)賦值語句中的目標(biāo)值并且v
是一個(gè)零值nil映射,則v[k]
在運(yùn)行時(shí)刻將造成一個(gè)恐慌;v[k]
用來表示讀取映射值v
中鍵值k
對(duì)應(yīng)的元素,則它無論如何都不會(huì)產(chǎn)生一個(gè)恐慌,即使v
是一個(gè)零值nil映射(假設(shè)k
的估值沒有造成恐慌);v[k]
用來表示讀取映射值v
中鍵值k
對(duì)應(yīng)的元素,并且映射值v
中并不含有對(duì)應(yīng)著鍵值k
的條目,則v[k]
返回一個(gè)此映射值的類型的元素類型的零值。 一般情況下,v[k]
被認(rèn)為是一個(gè)單值表達(dá)式。但是在一個(gè)v[k]
被用為唯一源值的賦值語句中,v[k]
可以返回一個(gè)可選的第二個(gè)返回值。
此第二個(gè)返回值是一個(gè)類型不確定布爾值,用來表示是否有對(duì)應(yīng)著鍵值k
的條目存儲(chǔ)在映射值v
中。一個(gè)展示了容器元素修改和讀取的例子:
package main
import "fmt"
func main() {
a := [3]int{-1, 0, 1}
s := []bool{true, false}
m := map[string]int{"abc": 123, "xyz": 789}
fmt.Println (a[2], s[1], m["abc"]) // 讀取
a[2], s[1], m["abc"] = 999, true, 567 // 修改
fmt.Println (a[2], s[1], m["abc"]) // 讀取
n, present := m["hello"]
fmt.Println(n, present, m["hello"]) // 0 false 0
n, present = m["abc"]
fmt.Println(n, present, m["abc"]) // 567 true 567
m = nil
fmt.Println(m["abc"]) // 0
// 下面這兩行編譯不通過。
/*
_ = a[3] // 下標(biāo)越界
_ = s[-1] // 下標(biāo)越界
*/
// 下面這幾行每行都會(huì)造成一個(gè)恐慌。
_ = a[n] // panic: 下標(biāo)越界
_ = s[n] // panic: 下標(biāo)越界
m["hello"] = 555 // panic: m為一個(gè)零值映射
}
為了更好的理解和解釋切片類型和切片值,我們最好對(duì)切片的內(nèi)部結(jié)構(gòu)有一個(gè)基本的印象。 在上一篇文章值部中,我們已經(jīng)了解到官方標(biāo)準(zhǔn)編譯器對(duì)切片類型的內(nèi)部定義大致如下:
type _slice struct {
elements unsafe.Pointer // 引用著底層存儲(chǔ)在間接部分上的元素
len int // 長(zhǎng)度
cap int // 容量
}
雖然其它編譯器中切片類型的內(nèi)部結(jié)構(gòu)可能并不完全和官方標(biāo)準(zhǔn)編譯器一致,但應(yīng)該大體上是相似的。 下面的解釋均基于官方標(biāo)準(zhǔn)編譯器對(duì)切片類型的內(nèi)部定義。
上面展示的切片的內(nèi)部定義為切片的直接部分的定義。直接部分的len
字段表示一個(gè)切片當(dāng)前存儲(chǔ)了多少個(gè)元素;直接部分的cap
表示一個(gè)切片的容量。 下面這張圖描繪了一個(gè)切片值的內(nèi)存布局。
盡管一個(gè)切片值的底層元素部分可能位于一個(gè)比較大的內(nèi)存片段上,但是此切片值只能感知到此內(nèi)存片段上的一個(gè)子片段。 比如,上圖中的切片值只能感知到灰色的子片段。
在上圖中,從下標(biāo)len
(包含)到下標(biāo)cap
(不包含)對(duì)應(yīng)的元素并不屬于圖中所示的切片值。 它們只是此切片之中的一些冗余元素槽位,但是它們可能是其它切片(或者數(shù)組)值中的有效元素。
下一節(jié)將要介紹如何通過調(diào)用內(nèi)置append
函數(shù)來向一個(gè)基礎(chǔ)切片添加元素而得到一個(gè)新的切片。 這個(gè)新的結(jié)果切片可能和這個(gè)基礎(chǔ)切片共享起始元素,也可能不共享,具體取決于基礎(chǔ)切片的容量(以及長(zhǎng)度)和添加的元素?cái)?shù)量。
當(dāng)一個(gè)切片被用做一個(gè)append
函數(shù)調(diào)用中的基礎(chǔ)切片,
下下一節(jié)將展示一張包含了上述兩種情況的圖片。
一些其它切片操作也可能會(huì)造成兩個(gè)切片共享底層內(nèi)存片段的情況。這些操作將在后續(xù)章節(jié)逐一介紹。
注意,一般我們不能單獨(dú)修改一個(gè)切片值的某個(gè)內(nèi)部字段,除非使用反射或者非類型安全指針。 換句話說,一般我們只能通過將其它切片賦值給一個(gè)切片來同時(shí)修改這個(gè)切片的三個(gè)字段。
當(dāng)一個(gè)映射賦值語句執(zhí)行完畢之后,目標(biāo)映射值和源映射值將共享底層的元素。 向其中一個(gè)映射中添加(或從中刪除)元素將體現(xiàn)在另一個(gè)映射中。
和映射一樣,當(dāng)一個(gè)切片賦值給另一個(gè)切片后,它們將共享底層的元素。它們的長(zhǎng)度和容量也相等。 但是和映射不同,如果以后其中一個(gè)切片改變了長(zhǎng)度或者容量,此變化不會(huì)體現(xiàn)到另一個(gè)切片中。
當(dāng)一個(gè)數(shù)組被賦值給另一個(gè)數(shù)組,所有的元素都將被從源數(shù)組復(fù)制到目標(biāo)數(shù)組。賦值完成之后,這兩個(gè)數(shù)組不共享任何元素。
一個(gè)例子:
package main
import "fmt"
func main() {
m0 := map[int]int{0:7, 1:8, 2:9}
m1 := m0
m1[0] = 2
fmt.Println(m0, m1) // map[0:2 1:8 2:9] map[0:2 1:8 2:9]
s0 := []int{7, 8, 9}
s1 := s0
s1[0] = 2
fmt.Println(s0, s1) // [2 8 9] [2 8 9]
a0 := [...]int{7, 8, 9}
a1 := a0
a1[0] = 2
fmt.Println(a0, a1) // [7 8 9] [2 8 9]
}
向一個(gè)映射中添加一個(gè)條目的語法和修改一個(gè)映射元素的語法是一樣的。 比如,對(duì)于一個(gè)非零映射值m
,如果當(dāng)前m
中尚未存儲(chǔ)條目(k, e)
,則下面的語法形式將把此條目存入m
;否則,下面的語法形式將把鍵值k
對(duì)應(yīng)的元素值更新為e
。
m[k] = e
內(nèi)置函數(shù)delete
用來從一個(gè)映射中刪除一個(gè)條目。比如,下面的delete
調(diào)用將把鍵值k
對(duì)應(yīng)的條目從映射m
中刪除。 如果映射m
中未存儲(chǔ)鍵值為k
的條目,則此調(diào)用為一個(gè)空操作,它不會(huì)產(chǎn)生一個(gè)恐慌,即使m
是一個(gè)nil零值映射。
delete(m, k)
下面的例子展示了如何向一個(gè)映射添加和從一個(gè)映射刪除條目。
package main
import "fmt"
func main() {
m := map[string]int{"Go": 2007}
m["C"] = 1972 // 添加
m["Java"] = 1995 // 添加
fmt.Println(m) // map[C:1972 Go:2007 Java:1995]
m["Go"] = 2009 // 修改
delete(m, "Java") // 刪除
fmt.Println(m) // map[C:1972 Go:2009]
}
注意,在Go 1.12之前,映射打印結(jié)果中的條目順序并不固定,兩次打印結(jié)果可能并不相同。
一個(gè)數(shù)組中的元素個(gè)數(shù)總是恒定的,我們無法向其中添加元素,也無法從其中刪除元素。但是可尋址的數(shù)組值中的元素是可以被修改的。
我們可以通過調(diào)用內(nèi)置append
函數(shù),以一個(gè)切片為基礎(chǔ),來添加不定數(shù)量的元素并返回一個(gè)新的切片。 此新的結(jié)果切片包含著基礎(chǔ)切片中所有的元素和所有被添加的元素。 注意,基礎(chǔ)切片并未被此append
函數(shù)調(diào)用所修改。 當(dāng)然,如果我們?cè)敢猓ㄊ聦?shí)上在實(shí)踐中常常如此),我們可以將結(jié)果切片賦值給基礎(chǔ)切片以修改基礎(chǔ)切片。
Go中并未提供一個(gè)內(nèi)置方式來從一個(gè)切片中刪除一個(gè)元素。 我們必須使用append
函數(shù)和后面將要介紹的子切片語法一起來實(shí)現(xiàn)元素刪除操作。 切片元素的刪除和插入將在后面的更多切片操作一節(jié)中介紹。 本節(jié)僅展示如何使用append
內(nèi)置函數(shù)。
下面是一個(gè)如何使用append
內(nèi)置函數(shù)的例子。
package main
import "fmt"
func main() {
s0 := []int{2, 3, 5}
fmt.Println(s0, cap(s0)) // [2 3 5] 3
s1 := append(s0, 7) // 添加一個(gè)元素
fmt.Println(s1, cap(s1)) // [2 3 5 7] 6
s2 := append(s1, 11, 13) // 添加兩個(gè)元素
fmt.Println(s2, cap(s2)) // [2 3 5 7 11 13] 6
s3 := append(s0) // <=> s3 := s0
fmt.Println(s3, cap(s3)) // [2 3 5] 3
s4 := append(s0, s0...) // 以s0為基礎(chǔ)添加s0中所有的元素
fmt.Println(s4, cap(s4)) // [2 3 5 2 3 5] 6
s0[0], s1[0] = 99, 789
fmt.Println(s2[0], s3[0], s4[0]) // 789 99 2
}
注意,內(nèi)置append
函數(shù)是一個(gè)變長(zhǎng)參數(shù)函數(shù)(下下篇文章中介紹)。 它有兩個(gè)參數(shù),其中第二個(gè)參數(shù)(形參)為一個(gè)變長(zhǎng)參數(shù)。
變長(zhǎng)參數(shù)函數(shù)將在下下篇文章中解釋。目前,我們只需知道變長(zhǎng)參數(shù)函數(shù)調(diào)用中的實(shí)參有兩種傳遞方式。 在上面的例子中,第8行、第10行和第12行使用了同一種方式,第14行使用了另外一種方式。 在第一種方式中,零個(gè)或多個(gè)實(shí)參元素值可以傳遞給append
函數(shù)的第二個(gè)形參。 在第二種方式中,一個(gè)(和第一個(gè)實(shí)參同元素類型的)實(shí)參切片傳遞給了第二個(gè)形參,此切片實(shí)參必須跟隨三個(gè)點(diǎn)...
。 關(guān)于變長(zhǎng)參數(shù)函數(shù)調(diào)用,詳見
下下篇文章。
在上例中,第14行等價(jià)于
s4 := append(s0, s0[0], s0[1], s0[2])
第8行等價(jià)于
s1 := append(s0, []int{7}...)
第10行等價(jià)于
s2 := append(s1, []int{11, 13}...)
對(duì)于三個(gè)點(diǎn)方式,append
函數(shù)并不要求第二個(gè)實(shí)參的類型和第一個(gè)實(shí)參一致,但是它們的元素類型必須一致。 換句話說,它們的底層類型必須一致。
在上面的程序中,
append
函數(shù)調(diào)用將為結(jié)果切片s1
開辟一段新的內(nèi)存。 原因是切片s0
中沒有足夠的冗余元素槽位來容納新添加的元素。 第14行的append
函數(shù)調(diào)用也是同樣的情況。append
函數(shù)調(diào)用不會(huì)為結(jié)果切片s2
開辟新的內(nèi)存片段。 原因是切片s1
中的冗余元素槽位足夠容納新添加的元素。所以,上面的程序中在退出之前,切片s1
和s2
共享一些元素,切片s0
和s3
共享所有的元素。 下面這張圖描繪了在上面的程序結(jié)束之前各個(gè)切片的狀態(tài)。
請(qǐng)注意,當(dāng)一個(gè)append
函數(shù)調(diào)用需要為結(jié)果切片開辟內(nèi)存時(shí),結(jié)果切片的容量取決于具體編譯器實(shí)現(xiàn)。 在這種情況下,對(duì)于官方標(biāo)準(zhǔn)編譯器,如果基礎(chǔ)切片的容量較小,則結(jié)果切片的容量至少為基礎(chǔ)切片的兩倍。 這樣做的目的是使結(jié)果切片有足夠多的冗余元素槽位,以防止此結(jié)果切片被用做后續(xù)其它append
函數(shù)調(diào)用的基礎(chǔ)切片時(shí)再次開辟內(nèi)存。
上面提到了,在實(shí)際編程中,我們常常將append
函數(shù)調(diào)用的結(jié)果賦值給基礎(chǔ)切片。 比如:
package main
import "fmt"
func main() {
var s = append([]string(nil), "array", "slice")
fmt.Println(s) // [array slice]
fmt.Println(cap(s)) // 2
s = append(s, "map")
fmt.Println(s) // [array slice map]
fmt.Println(cap(s)) // 4
s = append(s, "channel")
fmt.Println(s) // [array slice map channel]
fmt.Println(cap(s)) // 4
}
截至目前(Go 1.19),append
函數(shù)調(diào)用的第一個(gè)實(shí)參不能為類型不確定的nil
。
除了使用組合字面量來創(chuàng)建映射和切片,我們還可以使用內(nèi)置make
函數(shù)來創(chuàng)建映射和切片。 數(shù)組不能使用內(nèi)置make
函數(shù)來創(chuàng)建。
順便說一句,內(nèi)置make
函數(shù)也可以用來創(chuàng)建以后將要介紹的通道值。
假設(shè)M
是一個(gè)映射類型并且n
是一個(gè)整數(shù),我們可以用下面的兩種函數(shù)調(diào)用來各自生成一個(gè)類型為M
的映射值。
make(M, n)
make(M)
第一個(gè)函數(shù)調(diào)用形式創(chuàng)建了一個(gè)可以容納至少n
個(gè)條目而無需再次開辟內(nèi)存的空映射值。 第二個(gè)函數(shù)調(diào)用形式創(chuàng)建了一個(gè)可以容納一個(gè)小數(shù)目的條目而無需再次開辟內(nèi)存的空映射值。此小數(shù)目的值取決于具體編譯器實(shí)現(xiàn)。
注意:第二個(gè)參數(shù)n
可以為負(fù)或者零,這時(shí)對(duì)應(yīng)的調(diào)用將被視為上述第二種調(diào)用形式。
假設(shè)S
是一個(gè)切片類型,length
和capacity
是兩個(gè)非負(fù)整數(shù),并且length
小于等于capacity
,我們可以用下面的兩種函數(shù)調(diào)用來各自生成一個(gè)類型為S
的切片值。length
和capacity
的類型必須均為整數(shù)類型(兩者可以不一致)。
make(S, length, capacity)
make(S, length) // <=> make(S, length, length)
第一個(gè)函數(shù)調(diào)用創(chuàng)建了一個(gè)長(zhǎng)度為length
并且容量為capacity
的切片。 第二個(gè)函數(shù)調(diào)用創(chuàng)建了一個(gè)長(zhǎng)度為length
并且容量也為length
的切片。
使用make
函數(shù)創(chuàng)建的切片中的所有元素值均被初始化為(結(jié)果切片的元素類型的)零值。
下面是一個(gè)展示了如何使用make
函數(shù)來創(chuàng)建映射和切片的例子:
package main
import "fmt"
func main() {
// 創(chuàng)建映射。
fmt.Println(make(map[string]int)) // map[]
m := make(map[string]int, 3)
fmt.Println(m, len(m)) // map[] 0
m["C"] = 1972
m["Go"] = 2009
fmt.Println(m, len(m)) // map[C:1972 Go:2009] 2
// 創(chuàng)建切片。
s := make([]int, 3, 5)
fmt.Println(s, len(s), cap(s)) // [0 0 0] 3 5
s = make([]int, 2)
fmt.Println(s, len(s), cap(s)) // [0 0] 2 2
}
在前面的指針一文中,我們已經(jīng)了解到內(nèi)置new
函數(shù)可以用來為一個(gè)任何類型的值開辟內(nèi)存并返回一個(gè)存儲(chǔ)有此值的地址的指針。 用new
函數(shù)開辟出來的值均為零值。因?yàn)檫@個(gè)原因,new
函數(shù)對(duì)于創(chuàng)建映射和切片值來說沒有任何價(jià)值。
使用new
函數(shù)來用來創(chuàng)建數(shù)組值并非是完全沒有意義的,但是在實(shí)踐中很少這么做,因?yàn)槭褂媒M合字面量來創(chuàng)建數(shù)組值更為方便。
一個(gè)使用new
函數(shù)創(chuàng)建容器值的例子:
package main
import "fmt"
func main() {
m := *new(map[string]int) // <=> var m map[string]int
fmt.Println(m == nil) // true
s := *new([]int) // <=> var s []int
fmt.Println(s == nil) // true
a := *new([5]bool) // <=> var a [5]bool
fmt.Println(a == [5]bool{}) // true
}
一些關(guān)于容器元素的可尋址性的事實(shí):
一個(gè)例子:
package main
import "fmt"
func main() {
a := [5]int{2, 3, 5, 7}
s := make([]bool, 2)
pa2, ps1 := &a[2], &s[1]
fmt.Println(*pa2, *ps1) // 5 false
a[2], s[1] = 99, true
fmt.Println(*pa2, *ps1) // 99 true
ps0 := &[]string{"Go", "C"}[0]
fmt.Println(*ps0) // Go
m := map[int]bool{1: true}
_ = m
// 下面這幾行編譯不通過。
/*
_ = &[3]int{2, 3, 5}[0]
_ = &map[int]bool{1: true}[1]
_ = &m[1]
*/
}
一般來說,一個(gè)不可尋址的值的直接部分是不可修改的。但是映射元素是個(gè)例外。 映射元素雖然不可尋址,但是每個(gè)映射元素可以被整個(gè)修改(但不能被部分修改)。 對(duì)于大多數(shù)做為映射元素類型的類型,在修改它們的值的時(shí)候,一般體現(xiàn)不出來整個(gè)修改和部分修改的差異。 但是如果一個(gè)映射的元素類型為數(shù)組或者結(jié)構(gòu)體類型,這個(gè)差異是很明顯的。
在上一篇文章值部中,我們了解到每個(gè)數(shù)組或者結(jié)構(gòu)體值都是僅含有一個(gè)直接部分。所以
一個(gè)例子:
package main
import "fmt"
func main() {
type T struct{age int}
mt := map[string]T{}
mt["John"] = T{age: 29} // 整體修改是允許的
ma := map[int][5]int{}
ma[1] = [5]int{1: 789} // 整體修改是允許的
// 這兩個(gè)賦值編譯不通過,因?yàn)椴糠中薷囊粋€(gè)映射
// 元素是非法的。這看上去確實(shí)有些反直覺。
/*
ma[1][1] = 123 // error
mt["John"].age = 30 // error
*/
// 讀取映射元素的元素或者字段是沒問題的。
fmt.Println(ma[1][1]) // 789
fmt.Println(mt["John"].age) // 29
}
為了讓上例中的兩行編譯不通過的兩行賦值語句編譯通過,欲修改的映射元素必須先存放在一個(gè)臨時(shí)變量中,然后修改這個(gè)臨時(shí)變量,最后再用這個(gè)臨時(shí)變量整體覆蓋欲修改的映射元素。比如:
package main
import "fmt"
func main() {
type T struct{age int}
mt := map[string]T{}
mt["John"] = T{age: 29}
ma := map[int][5]int{}
ma[1] = [5]int{1: 789}
t := mt["John"] // 臨時(shí)變量
t.age = 30
mt["John"] = t // 整體修改
a := ma[1] // 臨時(shí)變量
a[1] = 123
ma[1] = a // 整體修改
fmt.Println(ma[1][1], mt["John"].age) // 123 30
}
注意:剛提到的這個(gè)限制可能會(huì)在以后被移除。
我們可以從一個(gè)基礎(chǔ)切片或者一個(gè)可尋址的基礎(chǔ)數(shù)組派生出另一個(gè)切片。此派生操作也常稱為一個(gè)取子切片操作。 派生出來的切片的元素和基礎(chǔ)切片(或者數(shù)組)的元素位于同一個(gè)內(nèi)存片段上。或者說,派生出來的切片和基礎(chǔ)切片(或者數(shù)組)將共享一些元素。
Go中有兩種取子切片的語法形式(假設(shè)baseContainer
是一個(gè)切片或者數(shù)組):
baseContainer[low : high] // 雙下標(biāo)形式
baseContainer[low : high : max] // 三下標(biāo)形式
上面所示的雙下標(biāo)形式等價(jià)于下面的三下標(biāo)形式:
baseContainer[low : high : cap(baseContainer)]
所以雙下標(biāo)形式是三下標(biāo)形式的特例。在實(shí)踐中,雙下標(biāo)形式使用得相對(duì)更為廣泛。
(注意:三下標(biāo)形式是從Go 1.2開始支持的。)
上面所示的取子切片表達(dá)式的語法形式中的下標(biāo)必須滿足下列關(guān)系,否則代碼要么編譯不通過,要么在運(yùn)行時(shí)刻將造成恐慌。
// 雙下標(biāo)形式
0 <= low <= high <= cap(baseContainer)
// 三下標(biāo)形式
0 <= low <= high <= max <= cap(baseContainer)
不滿足上述關(guān)系的取子切片表達(dá)式要么編譯不通過,要么在運(yùn)行時(shí)刻將導(dǎo)致一個(gè)恐慌。
注意:
low
和high
都可以大于len(baseContainer)
。但是它們一定不能大于cap(baseContainer)
。baseContainer
是一個(gè)零值nil切片,只要上面所示的子切片表達(dá)式中下標(biāo)的值均為0
,則這兩個(gè)子切片表達(dá)式不會(huì)造成恐慌。 在這種情況下,結(jié)果切片也是一個(gè)nil切片。子切片表達(dá)式的結(jié)果切片的長(zhǎng)度為high - low
、容量為max - low
。 派生出來的結(jié)果切片的長(zhǎng)度可能大于基礎(chǔ)切片的長(zhǎng)度,但結(jié)果切片的容量絕不可能大于基礎(chǔ)切片的容量。
在實(shí)踐中,我們常常在子切片表達(dá)式中省略若干下標(biāo),以使代碼看上去更加簡(jiǎn)潔。省略規(guī)則如下:
low
為零,則它可被省略。此條規(guī)則同時(shí)適用于雙下標(biāo)形式和三下標(biāo)形式。high
等于len(baseContainer)
,則它可被省略。此條規(guī)則同時(shí)只適用于雙下標(biāo)形式。max
在任何情況下都不可被省略。比如,下面的子切片表達(dá)式都是相互等價(jià)的:
baseContainer[0 : len(baseContainer)]
baseContainer[: len(baseContainer)]
baseContainer[0 :]
baseContainer[:]
baseContainer[0 : len(baseContainer) : cap(baseContainer)]
baseContainer[: len(baseContainer) : cap(baseContainer)]
一個(gè)使用了子切片語法的例子:
package main
import "fmt"
func main() {
a := [...]int{0, 1, 2, 3, 4, 5, 6}
s0 := a[:] // <=> s0 := a[0:7:7]
s1 := s0[:] // <=> s1 := s0
s2 := s1[1:3] // <=> s2 := a[1:3]
s3 := s1[3:] // <=> s3 := s1[3:7]
s4 := s0[3:5] // <=> s4 := s0[3:5:7]
s5 := s4[:2:2] // <=> s5 := s0[3:5:5]
s6 := append(s4, 77)
s7 := append(s5, 88)
s8 := append(s7, 66)
s3[1] = 99
fmt.Println(len(s2), cap(s2), s2) // 2 6 [1 2]
fmt.Println(len(s3), cap(s3), s3) // 4 4 [3 99 77 6]
fmt.Println(len(s4), cap(s4), s4) // 2 4 [3 99]
fmt.Println(len(s5), cap(s5), s5) // 2 2 [3 99]
fmt.Println(len(s6), cap(s6), s6) // 3 4 [3 99 77]
fmt.Println(len(s7), cap(s7), s7) // 3 4 [3 4 88]
fmt.Println(len(s8), cap(s8), s8) // 4 4 [3 4 88 66]
}
下面這張圖描繪了上面的程序在退出之前各個(gè)數(shù)組和切片的狀態(tài)。
從這張圖片可以看出,切片s7
和s8
共享存儲(chǔ)它們的元素的底層內(nèi)存片段,其它切片和數(shù)組a
共享同一個(gè)存儲(chǔ)元素的內(nèi)存片段。
請(qǐng)注意,子切片操作有可能會(huì)造成暫時(shí)性的內(nèi)存泄露。 比如,下面在這個(gè)函數(shù)中開辟的內(nèi)存塊中的前50個(gè)元素槽位在它的調(diào)用返回之后將不再可見。 這50個(gè)元素槽位所占內(nèi)存浪費(fèi)了,這屬于暫時(shí)性的內(nèi)存泄露。 當(dāng)這個(gè)函數(shù)中開辟的內(nèi)存塊今后不再被任何切片所引用,此內(nèi)存塊將被回收,這時(shí)內(nèi)存才不再繼續(xù)泄漏。
func f() []int {
s := make([]int, 10, 100)
return s[50:60]
}
請(qǐng)注意,在上面這個(gè)函數(shù)中,子切片表達(dá)式中的起始下標(biāo)(50
)比s
的長(zhǎng)度(10
)要大,這是允許的。
從Go 1.17開始,一個(gè)切片可以被轉(zhuǎn)化為一個(gè)相同元素類型的數(shù)組的指針類型。 但是如果數(shù)組的長(zhǎng)度大于被轉(zhuǎn)化切片的長(zhǎng)度,則將導(dǎo)致恐慌產(chǎn)生。 轉(zhuǎn)換結(jié)果和被轉(zhuǎn)化切片將共享底層元素。 一個(gè)例子:
package main
type S []int
type A [2]int
type P *A
func main() {
var x []int
var y = make([]int, 0)
var x0 = (*[0]int)(x) // okay, x0 == nil
var y0 = (*[0]int)(y) // okay, y0 != nil
_, _ = x0, y0
var z = make([]int, 3, 5)
var _ = (*[3]int)(z) // okay
var _ = (*[2]int)(z) // okay
var _ = (*A)(z) // okay
var _ = P(z) // okay
var w = S(z)
var _ = (*[3]int)(w) // okay
var _ = (*[2]int)(w) // okay
var _ = (*A)(w) // okay
var _ = P(w) // okay
var _ = (*[4]int)(z) // 會(huì)產(chǎn)生恐慌
}
從Go 1.20開始,一個(gè)切片可以被轉(zhuǎn)化為一個(gè)相同元素類型的數(shù)組。 但是如果數(shù)組的長(zhǎng)度大于被轉(zhuǎn)化切片的長(zhǎng)度,則將導(dǎo)致恐慌產(chǎn)生。 轉(zhuǎn)換過程中將復(fù)制所需的元素,因此結(jié)果數(shù)組和被轉(zhuǎn)化切片不共享底層元素。 一個(gè)例子:
package main
import "fmt"
func main() {
var s = []int{0, 1, 2, 3}
var a = [3]int(s[1:])
s[2] = 9
fmt.Println(s) // [0 1 9 3]
fmt.Println(a) // [1 2 3]
_ = [3]int(s[:2]) // panic
}
我們可以使用內(nèi)置copy
函數(shù)來將一個(gè)切片中的元素復(fù)制到另一個(gè)切片。 這兩個(gè)切片的類型可以不同,但是它們的元素類型必須相同。 換句話說,這兩個(gè)切片的類型的底層類型必須相同。 copy
函數(shù)的第一個(gè)參數(shù)為目標(biāo)切片,第二個(gè)參數(shù)為源切片。 傳遞給一個(gè)copy
函數(shù)調(diào)用的兩個(gè)實(shí)參可以共享一些底層元素。 copy
函數(shù)返回復(fù)制了多少個(gè)元素,此值(int
類型)為這兩個(gè)切片的長(zhǎng)度的較小值。
結(jié)合上一節(jié)介紹的子切片語法,我們可以使用copy
函數(shù)來在兩個(gè)數(shù)組之間或者一個(gè)數(shù)組與一個(gè)切片之間復(fù)制元素。
一個(gè)例子:
package main
import "fmt"
func main() {
type Ta []int
type Tb []int
dest := Ta{1, 2, 3}
src := Tb{5, 6, 7, 8, 9}
n := copy(dest, src)
fmt.Println(n, dest) // 3 [5 6 7]
n = copy(dest[1:], dest)
fmt.Println(n, dest) // 2 [5 5 6]
a := [4]int{} // 一個(gè)數(shù)組
n = copy(a[:], src)
fmt.Println(n, a) // 4 [5 6 7 8]
n = copy(a[:], a[2:])
fmt.Println(n, a) // 2 [7 8 7 8]
}
事實(shí)上,copy
并不是一個(gè)基本函數(shù)。我們可以用append
來實(shí)現(xiàn)它。
// 假設(shè)元素類型為T。
func Copy(dest, src []T) int {
if len(dest) < len(src) {
_ = append(dest[:0], src[:len(dest)]...)
return len(dest)
} else {
_ = append(dest[:0], src...)
return len(src)
}
}
盡管copy
函數(shù)不是一個(gè)基本函數(shù),它比上面的用append
的實(shí)現(xiàn)使用起來要方便得多。
從另外一個(gè)角度,我們也可以認(rèn)為append
不是一個(gè)基本函數(shù),因?yàn)槲覀兛梢杂?code>make加copy
函數(shù)來實(shí)現(xiàn)它。
注意,做為一個(gè)特例,copy
函數(shù)可以用來將一個(gè)字符串中的字節(jié)復(fù)制到一個(gè)字節(jié)切片。
截至目前(Go 1.19),copy
函數(shù)調(diào)用的兩個(gè)實(shí)參均不能為類型不確定的nil
。
在Go中,我們可以使用下面的語法形式來遍歷一個(gè)容器中的鍵值和元素:
for key, element = range aContainer {
// 使用key和element ...
}
在此語法形式中,for
和range
為兩個(gè)關(guān)鍵字,key
和element
稱為循環(huán)變量。 如果aContainer
是一個(gè)切片或者數(shù)組(或者數(shù)組指針,見后),則key
的類型必須為內(nèi)置類型int
。
上面所示的for-range
語法形式中的等號(hào)=
也可以是一個(gè)變量短聲明符號(hào):=
。 當(dāng)短聲明符號(hào)被使用的時(shí)候,key
和element
總是兩個(gè)新聲明的變量,這時(shí)如果aContainer
是一個(gè)切片或者數(shù)組(或者數(shù)組指針),則key
的類型被推斷為內(nèi)置類型int
。
和傳統(tǒng)的for
循環(huán)流程控制一樣,每個(gè)for-range
循環(huán)流程控制形成了兩個(gè)代碼塊,其中一個(gè)是隱式的,另一個(gè)是顯式的(花括號(hào)之間
的部分)。 此顯式的代碼塊內(nèi)嵌在隱式的代碼塊之中。
和for
循環(huán)流程控制一樣,break
和continue
也可以使用在一個(gè)for-range
循環(huán)流程控制中的顯式代碼塊中。
一個(gè)例子:
package main
import "fmt"
func main() {
m := map[string]int{"C": 1972, "C++": 1983, "Go": 2009}
for lang, year := range m {
fmt.Printf("%v: %v \n", lang, year)
}
a := [...]int{2, 3, 5, 7, 11}
for i, prime := range a {
fmt.Printf("%v: %v \n", i, prime)
}
s := []string{"go", "defer", "goto", "var"}
for i, keyword := range s {
fmt.Printf("%v: %v \n", i, keyword)
}
}
for-range
循環(huán)代碼塊有一些變種形式:
// 忽略鍵值循環(huán)變量。
for _, element = range aContainer {
// ...
}
// 忽略元素循環(huán)變量。
for key, _ = range aContainer {
element = aContainer[key]
// ...
}
// 舍棄元素循環(huán)變量。此形式和上一個(gè)變種等價(jià)。
for key = range aContainer {
element = aContainer[key]
// ...
}
// 鍵值和元素循環(huán)變量均被忽略。
for _, _ = range aContainer {
// 這個(gè)變種形式?jīng)]有太大實(shí)用價(jià)值。
}
// 鍵值和元素循環(huán)變量均被舍棄。此形式和上一個(gè)變種等價(jià)。
for range aContainer {
// 這個(gè)變種形式?jīng)]有太大實(shí)用價(jià)值。
}
遍歷一個(gè)nil映射或者nil切片是允許的。這樣的遍歷可以看作是一個(gè)空操作。
一些關(guān)于遍歷映射條目的細(xì)節(jié):
如果可以確保沒有其它協(xié)程操縱一個(gè)映射m
,則下面的代碼保證將清空m
中所有條目。
for key := range m {
delete(m, key)
}
當(dāng)然,數(shù)組和切片元素也可以用傳統(tǒng)的for
循環(huán)來遍歷。
for i := 0; i < len(anArrayOrSlice); i++ {
element := anArrayOrSlice[i]
// ...
}
對(duì)一個(gè)for-range
循環(huán)代碼塊
for key, element = range aContainer {...}
有三個(gè)重要的事實(shí)存在:
aContainer
的一個(gè)副本。
注意,只有aContainer
的直接部分被復(fù)制了。
此副本是一個(gè)匿名的值,所以它是不可被修改的。
aContainer
是一個(gè)數(shù)組,那么在遍歷過程中對(duì)此數(shù)組元素的修改不會(huì)體現(xiàn)到循環(huán)變量中。
原因是此數(shù)組的副本(被真正遍歷的容器)和此數(shù)組不共享任何元素。
aContainer
是一個(gè)切片(或者映射),那么在遍歷過程中對(duì)此切片(或者映射)元素的修改將體現(xiàn)到循環(huán)變量中。
原因是此切片(或者映射)的副本和此切片(或者映射)共享元素(或條目)。
aContainer
副本中的一個(gè)鍵值元素對(duì)將被賦值(復(fù)制)給循環(huán)變量。
所以對(duì)循環(huán)變量的直接部分的修改將不會(huì)體現(xiàn)在aContainer
中的對(duì)應(yīng)元素中。
(因?yàn)檫@個(gè)原因,并且for-range
循環(huán)是遍歷映射條目的唯一途徑,所以最好不要使用大尺寸的映射鍵值和元素類型,以避免較大的復(fù)制負(fù)擔(dān)。)
下面這個(gè)例子驗(yàn)證了上述第一個(gè)和第二個(gè)事實(shí)。
package main
import "fmt"
func main() {
type Person struct {
name string
age int
}
persons := [2]Person {{"Alice", 28}, {"Bob", 25}}
for i, p := range persons {
fmt.Println(i, p)
// 此修改將不會(huì)體現(xiàn)在這個(gè)遍歷過程中,
// 因?yàn)楸槐闅v的數(shù)組是persons的一個(gè)副本。
persons[1].name = "Jack"
// 此修改不會(huì)反映到persons數(shù)組中,因?yàn)閜
// 是persons數(shù)組的副本中的一個(gè)元素的副本。
p.age = 31
}
fmt.Println("persons:", &persons)
}
輸出結(jié)果:
0 {Alice 28}
1 {Bob 25}
persons: &[{Alice 28} {Jack 25}]
如果我們將上例中的數(shù)組改為一個(gè)切片,則在循環(huán)中對(duì)此切片的修改將在循環(huán)過程中體現(xiàn)出來。 但是對(duì)循環(huán)變量的修改仍然不會(huì)體現(xiàn)在此切片中。
...
// 改為一個(gè)切片。
persons := []Person {{"Alice", 28}, {"Bob", 25}}
for i, p := range persons {
fmt.Println(i, p)
// 這次,此修改將反映在此次遍歷過程中。
persons[1].name = "Jack"
// 這個(gè)修改仍然不會(huì)體現(xiàn)在persons切片容器中。
p.age = 31
}
fmt.Println("persons:", &persons)
}
輸出結(jié)果變成了:
0 {Alice 28}
1 {Jack 25}
persons: &[{Alice 28} {Jack 25}]
下面這個(gè)例子驗(yàn)證了上述的第二個(gè)和第三個(gè)事實(shí):
package main
import "fmt"
func main() {
langs := map[struct{ dynamic, strong bool }]map[string]int{
{true, false}: {"JavaScript": 1995},
{false, true}: {"Go": 2009},
{false, false}: {"C": 1972},
}
// 此映射的鍵值和元素類型均為指針類型。
// 這有些不尋常,只是為了講解目的。
m0 := map[*struct{ dynamic, strong bool }]*map[string]int{}
for category, langInfo := range langs {
m0[&category] = &langInfo
// 下面這行修改對(duì)映射langs沒有任何影響。
category.dynamic, category.strong = true, true
}
for category, langInfo := range langs {
fmt.Println(category, langInfo)
}
m1 := map[struct{ dynamic, strong bool }]map[string]int{}
for category, langInfo := range m0 {
m1[*category] = *langInfo
}
// 映射m0和m1中均只有一個(gè)條目。
fmt.Println(len(m0), len(m1)) // 1 1
fmt.Println(m1) // map[{true true}:map[C:1972]]
}
上面已經(jīng)提到了,映射條目的遍歷順序是隨機(jī)的。所以下面前三行的輸出順序可能會(huì)略有不同:
{false true} map[Go:2009]
{false false} map[C:1972]
{true false} map[JavaScript:1995]
1 1
map[{true true}:map[Go:2009]]
復(fù)制一個(gè)切片或者映射的代價(jià)很小,但是復(fù)制一個(gè)大尺寸的數(shù)組的代價(jià)比較大。 所以,一般來說,range
關(guān)鍵字后跟隨一個(gè)大尺寸數(shù)組不是一個(gè)好主意。 如果我們要遍歷一個(gè)大尺寸數(shù)組中的元素,我們以遍歷從此數(shù)組派生出來的一個(gè)切片,或者遍歷一個(gè)指向此數(shù)組的指針(詳見下一節(jié))。
對(duì)于一個(gè)數(shù)組或者切片,如果它的元素類型的尺寸較大,則一般來說,用第二個(gè)循環(huán)變量來存儲(chǔ)每個(gè)循環(huán)步中被遍歷的元素不是一個(gè)好主意。 對(duì)于這樣的數(shù)組或者切片,我們最好忽略或者舍棄for-range
代碼塊中的第二個(gè)循環(huán)變量,或者使用傳統(tǒng)的for
循環(huán)來遍歷元素。 比如,在下面這個(gè)例子中,函數(shù)fa
中的循環(huán)效率比函數(shù)fb
中的循環(huán)低得多。
type Buffer struct {
start, end int
data [1024]byte
}
func fa(buffers []Buffer) int {
numUnreads := 0
for _, buf := range buffers {
numUnreads += buf.end - buf.start
}
return numUnreads
}
func fb(buffers []Buffer) int {
numUnreads := 0
for i := range buffers {
numUnreads += buffers[i].end - buffers[i].start
}
return numUnreads
}
對(duì)于某些情形,我們可以把數(shù)組指針當(dāng)做數(shù)組來使用。
我們可以通過在range
關(guān)鍵字后跟隨一個(gè)數(shù)組的指針來遍歷此數(shù)組中的元素。 對(duì)于大尺寸的數(shù)組,這種方法比較高效,因?yàn)閺?fù)制一個(gè)指針比復(fù)制一個(gè)大尺寸數(shù)組的代價(jià)低得多。 下面的例子中的兩個(gè)循環(huán)是等價(jià)的,它們的效率也基本相同。
package main
import "fmt"
func main() {
var a [100]int
for i, n := range &a { // 復(fù)制一個(gè)指針的開銷很小
fmt.Println(i, n)
}
for i, n := range a[:] { // 復(fù)制一個(gè)切片的開銷很小
fmt.Println(i, n)
}
}
如果一個(gè)for-range
循環(huán)中的第二個(gè)循環(huán)變量既沒有被忽略,也沒有被舍棄,并且range
關(guān)鍵字后跟隨一個(gè)nil數(shù)組指針,則此循環(huán)將造成一個(gè)恐慌。 在下面這個(gè)例子中,前兩個(gè)循環(huán)都將打印出5個(gè)下標(biāo),但最后一個(gè)循環(huán)將導(dǎo)致一個(gè)恐慌。
package main
import "fmt"
func main() {
var p *[5]int // nil
for i, _ := range p { // okay
fmt.Println(i)
}
for i := range p { // okay
fmt.Println(i)
}
for i, n := range p { // panic
fmt.Println(i, n)
}
}
我們可以通過數(shù)組的指針來訪問和修改此數(shù)組中的元素。如果此指針是一個(gè)nil指針,將導(dǎo)致一個(gè)恐慌。
package main
import "fmt"
func main() {
a := [5]int{2, 3, 5, 7, 11}
p := &a
p[0], p[1] = 17, 19
fmt.Println(a) // [17 19 5 7 11]
p = nil
_ = p[0] // panic
}
我們可以從一個(gè)數(shù)組的指針派生出一個(gè)切片。從一個(gè)nil數(shù)組指針派生切片將導(dǎo)致一個(gè)恐慌。
package main
import "fmt"
func main() {
pa := &[5]int{2, 3, 5, 7, 11}
s := pa[1:3]
fmt.Println(s) // [3 5]
pa = nil
s = pa[0:0] // panic
// 如果下一行能被執(zhí)行到,則它也會(huì)產(chǎn)生恐慌。
_ = (*[0]byte)(nil)[:]
}
內(nèi)置len
和cap
函數(shù)調(diào)用接受數(shù)組指針做為實(shí)參。 nil數(shù)組指針實(shí)參不會(huì)導(dǎo)致恐慌。
var pa *[5]int // == nil
fmt.Println(len(pa), cap(pa)) // 5 5
假設(shè)t0
是一個(gè)類型T
的零值字面量,并且a
是一個(gè)元素類型為T
的數(shù)組或者切片,則官方標(biāo)準(zhǔn)編譯器將把下面的單循環(huán)變量for-range
代碼塊優(yōu)化為一個(gè)內(nèi)部的memclr
調(diào)用。 大多數(shù)情況下,此memclr
調(diào)用比一個(gè)一個(gè)地重置元素要快。
for i := range a {
a[i] = t0
}
此優(yōu)化在官方標(biāo)準(zhǔn)編譯器1.5版本中被引入。
從官方Go工具鏈1.19開始,此優(yōu)化也適用于a
為一個(gè)數(shù)組指針的情形。
如果傳遞給內(nèi)置函數(shù)len
或者cap
的一個(gè)調(diào)用的實(shí)參是一個(gè)數(shù)組或者數(shù)組指針,則此調(diào)用將在編譯時(shí)刻被估值。 此估值結(jié)果是一個(gè)類型為內(nèi)置類型int
的類型確定常量值。
一個(gè)例子:
package main
import "fmt"
var a [5]int
var p *[7]string
// N和M都是類型為int的類型確定值。
const N = len(a)
const M = cap(p)
func main() {
fmt.Println(N) // 5
fmt.Println(M) // 7
}
上面已經(jīng)提到了,一般來說,一個(gè)切片的長(zhǎng)度和容量不能被單獨(dú)修改。一個(gè)切片只有通過賦值的方式被整體修改。 但是,事實(shí)上,我們可以通過反射的途徑來單獨(dú)修改一個(gè)切片的長(zhǎng)度或者容量。 反射將在后面的一篇文章中詳解。
一個(gè)例子:
package main
import (
"fmt"
"reflect"
)
func main() {
s := make([]int, 2, 6)
fmt.Println(len(s), cap(s)) // 2 6
reflect.ValueOf(&s).Elem().SetLen(3)
fmt.Println(len(s), cap(s)) // 3 6
reflect.ValueOf(&s).Elem().SetCap(5)
fmt.Println(len(s), cap(s)) // 3 5
}
傳遞給函數(shù)reflect.SetLen
調(diào)用的第二個(gè)實(shí)參值必須不大于第一個(gè)實(shí)參切片值的容量。 傳遞給函數(shù)reflect.SetCap
調(diào)用的第二個(gè)實(shí)參值必須不小于第一個(gè)實(shí)參切片值的長(zhǎng)度并且須不大于第一個(gè)實(shí)參切片值的容量。 否則,在運(yùn)行時(shí)刻將產(chǎn)生一個(gè)恐慌。
此反射方法的效率很低,遠(yuǎn)低于一個(gè)切片的賦值。
Go不支持更多的內(nèi)置切片操作,比如切片克隆、元素刪除和插入。 我們必須用上面提到的各種內(nèi)置操作來實(shí)現(xiàn)這些操作。
在下面當(dāng)前大節(jié)中的例子中,假設(shè)s
是被談到的切片、T
是它的元素類型、t0
是類型T
的零值字面量。
對(duì)于當(dāng)前的Go版本(1.19),最簡(jiǎn)單的克隆一個(gè)切片的方法為:
sClone := append(s[:0:0], s...)
我們也可以使用下面這種實(shí)現(xiàn)。但是和上面這個(gè)實(shí)現(xiàn)相比,它有一個(gè)不完美之處:如果源切片s
是一個(gè)空切片(但是非nil),則結(jié)果切片是一個(gè)nil切片。
sClone := append([]T(nil), s...)
上面這兩種append實(shí)現(xiàn)都有一個(gè)缺點(diǎn):它們開辟的內(nèi)存塊常常會(huì)比需要的略大一些從而可能造成一點(diǎn)小小的不必要的性能損失。 我們可以使用這兩種方法來避免這個(gè)缺點(diǎn):
// 兩行make+copy實(shí)現(xiàn):
sClone := make([]T, len(s))
copy(sClone, s)
// 或者下面的make+append實(shí)現(xiàn)。
// 對(duì)于目前的官方Go工具鏈v1.19來說,這種
// 實(shí)現(xiàn)比上面的make+copy實(shí)現(xiàn)略慢一點(diǎn)。
sClone := append(make([]T, 0, len(s)), s...)
上面這兩種make方法都有一個(gè)缺點(diǎn):如果s
是一個(gè)nil切片,則使用此方法將得到一個(gè)非nil切片。 不過,在編程實(shí)踐中,我們常常并不需要追求克隆的完美性。如果我們確實(shí)需要,則需要多寫幾行:
var sClone []T
if s != nil {
sClone = make([]T, len(s))
copy(sClone, s)
}
在Go官方工具鏈1.15版本之前,對(duì)于一些常見的使用場(chǎng)景,使用append
來克隆切片比使用make
加copy
要高效得多。但是從1.15版本開始,官方標(biāo)準(zhǔn)編譯器對(duì)make+copy
這種方法做了特殊的優(yōu)化,從而使得此方法總是比使用append
來克隆切片高效。 但是請(qǐng)注意:此優(yōu)化只在被克隆的切片呈現(xiàn)為一個(gè)標(biāo)識(shí)符(包括限定標(biāo)識(shí)符)并且make
調(diào)用只有兩個(gè)實(shí)參的時(shí)候才有效。 比如,在下面的代碼中,它只在第一種情況中才有效:
// 情況一:
var s = make([]byte, 10000)
y = make([]T, len(s)) // works
copy(y, s)
// 情況二:
var s = make([]byte, 10000)
y = make([]T, len(s), len(s)) // not work
copy(y, s)
// 情況三:
var a = [1][]byte{s}
y = make([]T, len(a[0])) // not work
copy(y, a[0])
// 情況四:
type T struct {x []byte}
var t = T{x: s}
y = make([]T, len(t.x)) // not work
copy(y, t.x)
前面已經(jīng)提到了切片的元素在內(nèi)存中是連續(xù)存儲(chǔ)的,相鄰元素之間是沒有間隙的。所以,當(dāng)切片的一個(gè)元素段被刪除時(shí),
在下面的例子中,假設(shè)from
(包括)和to
(不包括)是兩個(gè)合法的下標(biāo),并且from
不大于to
。
// 第一種方法(保持剩余元素的次序):
s = append(s[:from], s[to:]...)
// 第二種方法(保持剩余元素的次序):
s = s[:from + copy(s[from:], s[to:])]
// 第三種方法(不保持剩余元素的次序):
if n := to-from; len(s)-to < n {
copy(s[from:to], s[to:])
} else {
copy(s[from:to], s[len(s)-n:])
}
s = s[:len(s)-(to-from)]
如果切片的元素可能引用著其它值,則我們應(yīng)該重置因?yàn)閯h除元素而多出來的元素槽位上的元素值,以避免暫時(shí)性的內(nèi)存泄露:
// "len(s)+to-from"是刪除操作之前切片s的長(zhǎng)度。
temp := s[len(s):len(s)+to-from]
for i := range temp {
temp[i] = t0 // t0是類型T的零值字面量
}
前面已經(jīng)提到了,上面這個(gè)for-range
循環(huán)將被官方標(biāo)準(zhǔn)編譯器優(yōu)化為一個(gè)memclr
調(diào)用。
刪除一個(gè)元素是刪除一個(gè)元素段的特例。在實(shí)現(xiàn)上可以簡(jiǎn)化一些。
在下面的例子中,假設(shè)i
將被刪除的元素的下標(biāo),并且它是一個(gè)合法的下標(biāo)。
// 第一種方法(保持剩余元素的次序):
s = append(s[:i], s[i+1:]...)
// 第二種方法(保持剩余元素的次序):
s = s[:i + copy(s[i:], s[i+1:])]
// 上面兩種方法都需要復(fù)制len(s)-i-1個(gè)元素。
// 第三種方法(不保持剩余元素的次序):
s[i] = s[len(s)-1]
s = s[:len(s)-1]
如果切片的元素可能引用著其它值,則我們應(yīng)該重置剛多出來的元素槽位上的元素值,以避免暫時(shí)性的內(nèi)存泄露:
s[len(s):len(s)+1][0] = t0
// 或者
s[:len(s)+1][len(s)] = t0
有時(shí),我們需要?jiǎng)h除滿足某些條件的切片元素。
// 假設(shè)T是一個(gè)小尺寸類型。
func DeleteElements(s []T, keep func(T) bool, clear bool) []T {
// result := make([]T, 0, len(s))
result := s[:0] // 無須開辟內(nèi)存
for _, v := range s {
if keep(v) {
result = append(result, v)
}
}
if clear { // 避免暫時(shí)性的內(nèi)存泄露。
temp := s[len(result):]
for i := range temp {
temp[i] = t0 // t0是類型T的零值
}
}
return result
}
注意:如果T
是一個(gè)大尺寸類型,請(qǐng)慎用T
做為參數(shù)類型和使用雙循環(huán)變量for-range
代碼塊遍歷元素類型為T
的切片。
假設(shè)插入位置i
是一個(gè)合法的下標(biāo)并且切片elements
中的元素將被插入到另一個(gè)切片s
中。
// 第一種方法:單行實(shí)現(xiàn)。
s = append(s[:i], append(elements, s[i:]...)...)
// 上面這種單行實(shí)現(xiàn)把s[i:]中的元素復(fù)制了兩次,并且它可能
// 最多導(dǎo)致兩次內(nèi)存開辟(最少一次)。
// 下面這種繁瑣的實(shí)現(xiàn)只把s[i:]中的元素復(fù)制了一次,并且
// 它最多只會(huì)導(dǎo)致一次內(nèi)存開辟(最少零次)。
// 但是,在當(dāng)前的官方標(biāo)準(zhǔn)編譯器實(shí)現(xiàn)中(1.19版本),此
// 繁瑣實(shí)現(xiàn)中的make調(diào)用將會(huì)把部分剛開辟出來的元素清零。
// 這其實(shí)是沒有必要的。所以此繁瑣實(shí)現(xiàn)并非總是比上面的
// 單行實(shí)現(xiàn)效率更高。事實(shí)上,它僅在處理小切片時(shí)更高效。
if cap(s) >= len(s) + len(elements) {
s = s[:len(s)+len(elements)]
copy(s[i+len(elements):], s[i:])
copy(s[i:], elements)
} else {
x := make([]T, 0, len(elements)+len(s))
x = append(x, s[:i]...)
x = append(x, elements...)
x = append(x, s[i:]...)
s = x
}
// Push(插入到結(jié)尾)。
s = append(s, elements...)
// Unshift(插入到開頭)。
s = append(elements, s...)
插入若干獨(dú)立的元素和插入一個(gè)切片中的所有元素類似。 我們可以使用切片組合字面量構(gòu)建一個(gè)臨時(shí)切片,然后使用上面的方法插入這些元素。
假設(shè)被推入和彈出的元素為e
并且切片s
擁有至少一個(gè)元素。
// 前彈出(pop front,又稱shift)
s, e = s[1:], s[0]
// 后彈出(pop back)
s, e = s[:len(s)-1], s[len(s)-1]
// 前推(push front)
s = append([]T{e}, s...)
// 后推(push back)
s = append(s, e)
請(qǐng)注意:使用append
函數(shù)來插入元素常常是比較低效的,因?yàn)椴迦朦c(diǎn)后的所有元素都要向后挪,并且當(dāng)空余容量不足時(shí)還需要開辟一個(gè)更大的內(nèi)存空間來容納插入完成后所有的元素。 對(duì)于元素個(gè)數(shù)不多的切片來說,這些可能并不是嚴(yán)重的問題;但是在元素個(gè)數(shù)很多的切片上進(jìn)行如上的插入操作常常是耗時(shí)的。所以如果元素個(gè)數(shù)很多,最好使用鏈表來實(shí)現(xiàn)元素插入操作。
在實(shí)踐中,需求是各種各樣的。對(duì)于某些特定的情形,上面的例子中的代碼實(shí)現(xiàn)可能并非是最優(yōu)化的,甚至是不滿足要求的。 所以,請(qǐng)?jiān)趯?shí)踐中根據(jù)具體情況來實(shí)現(xiàn)代碼。或許,這就是Go沒有支持更多的內(nèi)置切片操作的原因。
Go不支持內(nèi)置集合(set)類型。但是,集合類型可以用輕松使用映射類型來模擬。 在實(shí)踐中,我們常常使用映射類型map[K]struct{}
來模擬一個(gè)元素類型為K
的集合類型。 類型struct{}
的尺寸為零,所以此映射類型的值中的元素不消耗內(nèi)存。
請(qǐng)注意,上述所有各種容器操作的內(nèi)部實(shí)現(xiàn)都未進(jìn)行同步。如果不使用今后將要介紹的各種并發(fā)同步技術(shù),在沒有協(xié)程修改一個(gè)容器值和它的元素的時(shí)候,多個(gè)協(xié)程并發(fā)讀取此容器值和它的元素是安全的。但是并發(fā)修改同一個(gè)容器值則是不安全的。 不使用并發(fā)同步技術(shù)而并發(fā)修改同一個(gè)容器值將會(huì)造成數(shù)據(jù)競(jìng)爭(zhēng)。請(qǐng)閱讀以后的并發(fā)同步概述一文以了解Go支持的各種并發(fā)同步技術(shù)。
更多建議: