Go語言 數(shù)組、切片和映射 - Go中的首要容器類型

2023-02-16 17:37 更新

在嚴(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):

  • 映射的鍵值類型可以是任何可比較類型。
  • 對(duì)于大多數(shù)元素為零值的情況,使用映射可以節(jié)省大量的內(nèi)存。

從上一篇文章中,我們已經(jīng)了解到,在任何賦值中,源值的底層間接部分不會(huì)被復(fù)制。 換句話說,當(dāng)一個(gè)賦值結(jié)束后,一個(gè)含有間接部分的源值和目標(biāo)值將共享底層間接部分。 這就是數(shù)組和切片/映射值會(huì)有很多行為差異(將在下面逐一介紹)的原因。

無名容器類型的字面表示形式

無名容器類型的字面表示形式如下:

  • 數(shù)組類型:[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ù)組或者切片組合字面量中:

  • 如果一個(gè)索引下標(biāo)出現(xiàn),它的類型不必是數(shù)組和切片類型的鍵值類型int,但它必須是一個(gè)可以表示為int值的非負(fù)常量; 如果它是一個(gè)類型確定值,則它的類型必須為一個(gè)基本整數(shù)類型。
  • 在一個(gè)數(shù)組或切片組合字面量中,如果一個(gè)元素的索引下標(biāo)缺失,則編譯器認(rèn)為它的索引下標(biāo)為出現(xiàn)在它之前的元素的索引下標(biāo)加一。
  • 如果出現(xiàn)的第一個(gè)元素的索引下標(biāo)缺失,則它的索引下標(biāo)被認(rèn)為是0。

映射組合字面量中元素對(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)嵌在其它組合字面量中的組合字面量可以簡(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)度和容量

除了上面已提到的容器長(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è)零值映射
}

重溫一下切片的內(nèi)部結(jié)構(gòu)

為了更好的理解和解釋切片類型和切片值,我們最好對(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ǔ)切片,

  • 如果添加的元素?cái)?shù)量大于此(基礎(chǔ))切片的冗余元素槽位的數(shù)量,則一個(gè)新的底層內(nèi)存片段將被開辟出來并用來存放結(jié)果切片的元素。 這時(shí),基礎(chǔ)切片和結(jié)果切片不共享任何底層元素。
  • 否則,不會(huì)有底層內(nèi)存片段被開辟出來。這時(shí),基礎(chǔ)切片中的所有元素也同時(shí)屬于結(jié)果切片。兩個(gè)切片的元素都存放于同一個(gè)內(nèi)存片段上。

下下一節(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í)參一致,但是它們的元素類型必須一致。 換句話說,它們的底層類型必須一致。

在上面的程序中,

  • 8行的append函數(shù)調(diào)用將為結(jié)果切片s1開辟一段新的內(nèi)存。 原因是切片s0中沒有足夠的冗余元素槽位來容納新添加的元素。 第14行的append函數(shù)調(diào)用也是同樣的情況。
  • 10行的append函數(shù)調(diào)用不會(huì)為結(jié)果切片s2開辟新的內(nèi)存片段。 原因是切片s1中的冗余元素槽位足夠容納新添加的元素。

所以,上面的程序中在退出之前,切片s1s2共享一些元素,切片s0s3共享所有的元素。 下面這張圖描繪了在上面的程序結(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。

使用內(nèi)置make函數(shù)來創(chuàng)建切片和映射

除了使用組合字面量來創(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è)切片類型,lengthcapacity是兩個(gè)非負(fù)整數(shù),并且length小于等于capacity,我們可以用下面的兩種函數(shù)調(diào)用來各自生成一個(gè)類型為S的切片值。lengthcapacity的類型必須均為整數(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
}

使用內(nèi)置new函數(shù)來創(chuàng)建容器值

在前面的指針一文中,我們已經(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è)數(shù)組是可尋址的,則它的元素也是可尋址的;反之亦然,即如果一個(gè)數(shù)組是不可尋址的,則它的元素也是不可尋址的。 原因很簡(jiǎn)單,因?yàn)橐粋€(gè)數(shù)組只含有一個(gè)(直接)值部,并且它的所有元素和此直接值部均承載在同一個(gè)內(nèi)存塊上。
  • 一個(gè)切片值的任何元素都是可尋址的,即使此切片本身是不可尋址的。 這是因?yàn)橐粋€(gè)切片的底層元素總是存儲(chǔ)在一個(gè)被開辟出來的內(nèi)存片段(間接值部)上。
  • 任何映射元素都是不可尋址的。原因詳見此條問答。

一個(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è)映射類型的元素類型為一個(gè)結(jié)構(gòu)體類型,則我們無法修改此映射類型的值中的每個(gè)結(jié)構(gòu)體元素的單個(gè)字段。 我們必須整體地同時(shí)修改所有結(jié)構(gòu)體字段。
  • 如果一個(gè)映射類型的元素類型為一個(gè)數(shù)組類型,則我們無法修改此映射類型的值中的每個(gè)數(shù)組元素的單個(gè)元素。 我們必須整體地同時(shí)修改所有數(shù)組元素。

一個(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ì)在以后被移除

從數(shù)組或者切片派生切片(取子切片)

我們可以從一個(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è)恐慌。

注意:

  • 只要上述關(guān)系均滿足,下標(biāo)lowhigh都可以大于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ī)則如下:

  • 如果下標(biāo)low為零,則它可被省略。此條規(guī)則同時(shí)適用于雙下標(biāo)形式和三下標(biāo)形式。
  • 如果下標(biāo)high等于len(baseContainer),則它可被省略。此條規(guī)則同時(shí)只適用于雙下標(biāo)形式。
  • 三下標(biāo)形式中的下標(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)。


從這張圖片可以看出,切片s7s8共享存儲(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)要大,這是允許的。

切片轉(zhuǎn)化為數(shù)組指針

從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)生恐慌
}

切片轉(zhuǎn)化為數(shù)組

從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ù)來復(fù)制切片元素

我們可以使用內(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 ...
}

在此語法形式中,forrange為兩個(gè)關(guān)鍵字,keyelement稱為循環(huán)變量。 如果aContainer是一個(gè)切片或者數(shù)組(或者數(shù)組指針,見后),則key的類型必須為內(nèi)置類型int。

上面所示的for-range語法形式中的等號(hào)=也可以是一個(gè)變量短聲明符號(hào):=。 當(dāng)短聲明符號(hào)被使用的時(shí)候,keyelement總是兩個(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)流程控制一樣,breakcontinue也可以使用在一個(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é):

  • 映射中的條目的遍歷順序是不確定的(可以認(rèn)為是隨機(jī)的)。或者說,同一個(gè)映射中的條目的兩次遍歷中,條目的順序很可能是不一致的,即使在這兩次遍歷之間,此映射并未發(fā)生任何改變。
  • 如果在一個(gè)映射中的條目的遍歷過程中,一個(gè)還沒有被遍歷到的條目被刪除了,則此條目保證不會(huì)被遍歷出來。
  • 如果在一個(gè)映射中的條目的遍歷過程中,一個(gè)新的條目被添加入此映射,則此條目并不保證將在此遍歷過程中被遍歷出來。

如果可以確保沒有其它協(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í)存在:

  1. 被遍歷的容器值是aContainer一個(gè)副本。 注意,只有aContainer的直接部分被復(fù)制了。 此副本是一個(gè)匿名的值,所以它是不可被修改的。
    • 如果aContainer是一個(gè)數(shù)組,那么在遍歷過程中對(duì)此數(shù)組元素的修改不會(huì)體現(xiàn)到循環(huán)變量中。 原因是此數(shù)組的副本(被真正遍歷的容器)和此數(shù)組不共享任何元素。
    • 如果aContainer是一個(gè)切片(或者映射),那么在遍歷過程中對(duì)此切片(或者映射)元素的修改將體現(xiàn)到循環(huán)變量中。 原因是此切片(或者映射)的副本和此切片(或者映射)共享元素(或條目)。
  2. 在遍歷中的每個(gè)循環(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)。)
  3. 所有被遍歷的鍵值對(duì)將被賦值給同一對(duì)循環(huán)變量實(shí)例。

下面這個(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
}

把數(shù)組指針當(dāng)做數(shù)組來使用

對(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)置lencap函數(shù)調(diào)用接受數(shù)組指針做為實(shí)參。 nil數(shù)組指針實(shí)參不會(huì)導(dǎo)致恐慌。

var pa *[5]int // == nil
fmt.Println(len(pa), cap(pa)) // 5 5

memclr優(yōu)化

假設(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的調(diào)用可能會(huì)在編譯時(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
}

單獨(dú)修改一個(gè)切片的長(zhǎng)度或者容量

上面已經(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來克隆切片比使用makecopy高效得多。但是從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í),

  • 如果剩余元素的次序必須保持原樣,則被刪除的元素段后面的每個(gè)元素都得前移。
  • 如果剩余元素的次序不需要保持原樣,則我們可以將尾部的一些元素移到被刪除的元素的位置上。

在下面的例子中,假設(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è)元素是刪除一個(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的切片。

將一個(gè)切片中的所有元素插入到另一個(gè)切片中

假設(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ú)立的元素

插入若干獨(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)元素插入操作。

關(guān)于上面各種切片操控的例子

在實(shí)踐中,需求是各種各樣的。對(duì)于某些特定的情形,上面的例子中的代碼實(shí)現(xiàn)可能并非是最優(yōu)化的,甚至是不滿足要求的。 所以,請(qǐng)?jiān)趯?shí)踐中根據(jù)具體情況來實(shí)現(xiàn)代碼。或許,這就是Go沒有支持更多的內(nèi)置切片操作的原因。

用映射來模擬集合(set)

Go不支持內(nèi)置集合(set)類型。但是,集合類型可以用輕松使用映射類型來模擬。 在實(shí)踐中,我們常常使用映射類型map[K]struct{}來模擬一個(gè)元素類型為K的集合類型。 類型struct{}的尺寸為零,所以此映射類型的值中的元素不消耗內(nèi)存。

上述各種容器操作內(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ù)。


以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)