第03章 Go语言容器(container)
變量在一定程度上能滿(mǎn)足函數(shù)及代碼要求。如果編寫(xiě)一些復(fù)雜算法、結(jié)構(gòu)和邏輯,就需要更復(fù)雜的類(lèi)型來(lái)實(shí)現(xiàn)。這類(lèi)復(fù)雜類(lèi)型一般情況下具有各種形式的存儲(chǔ)和處理數(shù)據(jù)的功能,將它們稱(chēng)為“容器(container)”。
在很多語(yǔ)言里,容器是以標(biāo)準(zhǔn)庫(kù)的方式提供,你可以隨時(shí)查看這些標(biāo)準(zhǔn)庫(kù)的代碼,了解如何創(chuàng)建,刪除,維護(hù)內(nèi)存。
本章將以實(shí)用為目的,詳細(xì)介紹數(shù)組、切片、映射,以及列表的增加、刪除、修改和遍歷的使用方法。本章既可以作為教程,也可以作為字典,以方便開(kāi)發(fā)者日常的查詢(xún)和應(yīng)用。
其它語(yǔ)言中的容器
- C語(yǔ)言沒(méi)有提供容器封裝,開(kāi)發(fā)者需要自己根據(jù)性能需求進(jìn)行封裝,或者使用第三方提供的容器。
- C++ 語(yǔ)言的容器通過(guò)標(biāo)準(zhǔn)庫(kù)提供,如 vector 對(duì)應(yīng)數(shù)組,list 對(duì)應(yīng)雙鏈表,map 對(duì)應(yīng)映射等。
- C# 語(yǔ)言通過(guò) .NET 框架提供,如 List 對(duì)應(yīng)數(shù)組,LinkedList 對(duì)應(yīng)雙鏈表,Dictionary 對(duì)應(yīng)映射。
- Lua 語(yǔ)言的 table 實(shí)現(xiàn)了數(shù)組和映射的功能,Lua 語(yǔ)言默認(rèn)沒(méi)有雙鏈表支持。
3.1?Go語(yǔ)言數(shù)組詳解
數(shù)組是一個(gè)由固定長(zhǎng)度的特定類(lèi)型元素組成的序列,一個(gè)數(shù)組可以由零個(gè)或多個(gè)元素組成。因?yàn)閿?shù)組的長(zhǎng)度是固定的,所以在Go語(yǔ)言中很少直接使用數(shù)組。
和數(shù)組對(duì)應(yīng)的類(lèi)型是 Slice(切片),Slice 是可以增長(zhǎng)和收縮的動(dòng)態(tài)序列,功能也更靈活,但是想要理解 slice 工作原理的話(huà)需要先理解數(shù)組,所以本節(jié)主要為大家講解數(shù)組的使用,至于 Slice(切片)將在《Go語(yǔ)言切片》一節(jié)中為大家講解。
Go語(yǔ)言數(shù)組的聲明
數(shù)組的聲明語(yǔ)法如下:
var 數(shù)組變量名 [元素?cái)?shù)量]Type
語(yǔ)法說(shuō)明如下所示:
- 數(shù)組變量名:數(shù)組聲明及使用時(shí)的變量名。
- 元素?cái)?shù)量:數(shù)組的元素?cái)?shù)量,可以是一個(gè)表達(dá)式,但最終通過(guò)編譯期計(jì)算的結(jié)果必須是整型數(shù)值,元素?cái)?shù)量不能含有到運(yùn)行時(shí)才能確認(rèn)大小的數(shù)值。
- Type:可以是任意基本類(lèi)型,包括數(shù)組本身,類(lèi)型為數(shù)組本身時(shí),可以實(shí)現(xiàn)多維數(shù)組。
數(shù)組的每個(gè)元素都可以通過(guò)索引下標(biāo)來(lái)訪問(wèn),索引下標(biāo)的范圍是從 0 開(kāi)始到數(shù)組長(zhǎng)度減 1 的位置,內(nèi)置函數(shù)?len() 可以返回?cái)?shù)組中元素的個(gè)數(shù)。
默認(rèn)情況下,數(shù)組的每個(gè)元素都會(huì)被初始化為元素類(lèi)型對(duì)應(yīng)的零值,對(duì)于數(shù)字類(lèi)型來(lái)說(shuō)就是 0,同時(shí)也可以使用數(shù)組字面值語(yǔ)法,用一組值來(lái)初始化數(shù)組:
在數(shù)組的定義中,如果在數(shù)組長(zhǎng)度的位置出現(xiàn)“...”省略號(hào),則表示數(shù)組的長(zhǎng)度是根據(jù)初始化值的個(gè)數(shù)來(lái)計(jì)算,因此,上面數(shù)組?q 的定義可以簡(jiǎn)化為:
數(shù)組的長(zhǎng)度是數(shù)組類(lèi)型的一個(gè)組成部分,因此 [3]int 和 [4]int 是兩種不同的數(shù)組類(lèi)型,數(shù)組的長(zhǎng)度必須是常量表達(dá)式,因?yàn)閿?shù)組的長(zhǎng)度需要在編譯階段確定。
比較兩個(gè)數(shù)組是否相等
如果兩個(gè)數(shù)組類(lèi)型相同(包括數(shù)組的長(zhǎng)度,數(shù)組中元素的類(lèi)型)的情況下,我們可以直接通過(guò)較運(yùn)算符(==和!=)來(lái)判斷兩個(gè)數(shù)組是否相等,只有當(dāng)兩個(gè)數(shù)組的所有元素都是相等的時(shí)候數(shù)組才是相等的,不能比較兩個(gè)類(lèi)型不同的數(shù)組,否則程序?qū)o(wú)法完成編譯。
遍歷數(shù)組——訪問(wèn)每一個(gè)數(shù)組元素
遍歷數(shù)組也和遍歷切片類(lèi)似,代碼如下所示:
代碼輸出結(jié)果:
0 hammer
1 soldier
2 mum
代碼說(shuō)明如下:
- 第 6 行,使用 for 循環(huán),遍歷 team 數(shù)組,遍歷出的鍵 k 為數(shù)組的索引,值 v 為數(shù)組的每個(gè)元素值。
- 第 7 行,將每個(gè)鍵值打印出來(lái)。
3.2?Go語(yǔ)言多維數(shù)組簡(jiǎn)述
Go語(yǔ)言中允許使用多維數(shù)組,因?yàn)閿?shù)組屬于值類(lèi)型,所以多維數(shù)組的所有維度都會(huì)在創(chuàng)建時(shí)自動(dòng)初始化零值,多維數(shù)組尤其適合管理具有父子關(guān)系或者與坐標(biāo)系相關(guān)聯(lián)的數(shù)據(jù)。
聲明多維數(shù)組的語(yǔ)法如下所示:
var array_name [size1][size2]...[sizen] array_type
其中,array_name 為數(shù)組的名字,array_type 為數(shù)組的類(lèi)型,size1、size2 等等為數(shù)組每一維度的長(zhǎng)度。
結(jié)合上一節(jié)《Go語(yǔ)言數(shù)組》中所學(xué)到的知識(shí),下面以二維數(shù)組為例來(lái)簡(jiǎn)單講解一下多維數(shù)組的使用。
二維數(shù)組是最簡(jiǎn)單的多維數(shù)組,二維數(shù)組本質(zhì)上是由多個(gè)一維數(shù)組組成的。
【示例 1】聲明二維數(shù)組
下圖展示了上面示例中聲明的二維數(shù)組在每次聲明并初始化后包含的值。
?
圖:二維數(shù)組及其外層數(shù)組和內(nèi)層數(shù)組的值
為了訪問(wèn)單個(gè)元素,需要反復(fù)組合使用[ ]方括號(hào),如下所示。
【示例 2】為二維數(shù)組的每個(gè)元素賦值
只要類(lèi)型一致,就可以將多維數(shù)組互相賦值,如下所示,多維數(shù)組的類(lèi)型包括每一維度的長(zhǎng)度以及存儲(chǔ)在元素中數(shù)據(jù)的類(lèi)型。
【示例 3】同樣類(lèi)型的多維數(shù)組賦值
因?yàn)閿?shù)組中每個(gè)元素都是一個(gè)值,所以可以獨(dú)立復(fù)制某個(gè)維度,如下所示。
【示例 4】使用索引為多維數(shù)組賦值
3.3?Go語(yǔ)言切片詳解
切片(slice)是對(duì)數(shù)組的一個(gè)連續(xù)片段的引用,所以切片是一個(gè)引用類(lèi)型(因此更類(lèi)似于 C/C++?中的數(shù)組類(lèi)型,或者?Python?中的 list 類(lèi)型),這個(gè)片段可以是整個(gè)數(shù)組,也可以是由起始和終止索引標(biāo)識(shí)的一些項(xiàng)的子集,需要注意的是,終止索引標(biāo)識(shí)的項(xiàng)不包括在切片內(nèi)。
Go語(yǔ)言中切片的內(nèi)部結(jié)構(gòu)包含地址、大小和容量,切片一般用于快速地操作一塊數(shù)據(jù)集合,如果將數(shù)據(jù)集合比作切糕的話(huà),切片就是你要的“那一塊”,切的過(guò)程包含從哪里開(kāi)始(切片的起始位置)及切多大(切片的大小),容量可以理解為裝切片的口袋大小,如下圖所示。
?
圖:切片結(jié)構(gòu)和內(nèi)存分配
從數(shù)組或切片生成新的切片
切片默認(rèn)指向一段連續(xù)內(nèi)存區(qū)域,可以是數(shù)組,也可以是切片本身。
從連續(xù)內(nèi)存區(qū)域生成切片是常見(jiàn)的操作,格式如下:
slice [開(kāi)始位置 : 結(jié)束位置]
語(yǔ)法說(shuō)明如下:
- slice:表示目標(biāo)切片對(duì)象;
- 開(kāi)始位置:對(duì)應(yīng)目標(biāo)切片對(duì)象的索引;
- 結(jié)束位置:對(duì)應(yīng)目標(biāo)切片的結(jié)束索引。
從數(shù)組生成切片,代碼如下:
其中 a 是一個(gè)擁有 3 個(gè)整型元素的數(shù)組,被初始化為數(shù)值 1 到 3,使用 a[1:2] 可以生成一個(gè)新的切片,代碼運(yùn)行結(jié)果如下:
[1 2 3] ?[2]
其中 [2] 就是 a[1:2] 切片操作的結(jié)果。
從數(shù)組或切片生成新的切片擁有如下特性:
- 取出的元素?cái)?shù)量為:結(jié)束位置 - 開(kāi)始位置;
- 取出元素不包含結(jié)束位置對(duì)應(yīng)的索引,切片最后一個(gè)元素使用 slice[len(slice)] 獲取;
- 當(dāng)缺省開(kāi)始位置時(shí),表示從連續(xù)區(qū)域開(kāi)頭到結(jié)束位置;
- 當(dāng)缺省結(jié)束位置時(shí),表示從開(kāi)始位置到整個(gè)連續(xù)區(qū)域末尾;
- 兩者同時(shí)缺省時(shí),與切片本身等效;
- 兩者同時(shí)為 0 時(shí),等效于空切片,一般用于切片復(fù)位。
根據(jù)索引位置取切片 slice 元素值時(shí),取值范圍是(0~len(slice)-1),超界會(huì)報(bào)運(yùn)行時(shí)錯(cuò)誤,生成切片時(shí),結(jié)束位置可以填寫(xiě) len(slice) 但不會(huì)報(bào)錯(cuò)。
下面通過(guò)實(shí)例來(lái)熟悉切片的特性。
1) 從指定范圍中生成切片
切片和數(shù)組密不可分,如果將數(shù)組理解為一棟辦公樓,那么切片就是把不同的連續(xù)樓層出租給使用者,出租的過(guò)程需要選擇開(kāi)始樓層和結(jié)束樓層,這個(gè)過(guò)程就會(huì)生成切片,示例代碼如下:
代碼輸出如下:
[11 12 13 14 15]
[21 22 23 24 25 26 27 28 29 30]
[1 2]
代碼中構(gòu)建了一個(gè) 30 層的高層建筑,數(shù)組的元素值從 1 到 30,分別代表不同的獨(dú)立樓層,輸出的結(jié)果是不同的租售方案。
代碼說(shuō)明如下:
- 第 8 行,嘗試出租一個(gè)區(qū)間樓層。
- 第 11 行,出租 20 層以上。
- 第 14 行,出租 2 層以下,一般是商用鋪面。
切片有點(diǎn)像C語(yǔ)言里的指針,指針可以做運(yùn)算,但代價(jià)是內(nèi)存操作越界,切片在指針的基礎(chǔ)上增加了大小,約束了切片對(duì)應(yīng)的內(nèi)存區(qū)域,切片使用中無(wú)法對(duì)切片內(nèi)部的地址和大小進(jìn)行手動(dòng)調(diào)整,因此切片比指針更安全、強(qiáng)大。
2) 表示原有的切片
生成切片的格式中,當(dāng)開(kāi)始和結(jié)束位置都被忽略時(shí),生成的切片將表示和原切片一致的切片,并且生成的切片與原切片在數(shù)據(jù)內(nèi)容上也是一致的,代碼如下:
a 是一個(gè)擁有 3 個(gè)元素的切片,將 a 切片使用 a[:] 進(jìn)行操作后,得到的切片與 a 切片一致,代碼輸出如下:
[1 2 3]
3) 重置切片,清空擁有的元素
把切片的開(kāi)始和結(jié)束位置都設(shè)為 0 時(shí),生成的切片將變空,代碼如下:
代碼輸出如下:
[]
直接聲明新的切片
除了可以從原有的數(shù)組或者切片中生成切片外,也可以聲明一個(gè)新的切片,每一種類(lèi)型都可以擁有其切片類(lèi)型,表示多個(gè)相同類(lèi)型元素的連續(xù)集合,因此切片類(lèi)型也可以被聲明,切片類(lèi)型聲明格式如下:
var name []Type
其中 name 表示切片的變量名,Type 表示切片對(duì)應(yīng)的元素類(lèi)型。
下面代碼展示了切片聲明的使用過(guò)程:
代碼輸出結(jié)果:
[] [] []
0 0 0
true
true
false
代碼說(shuō)明如下:
- 第 2 行,聲明一個(gè)字符串切片,切片中擁有多個(gè)字符串。
- 第 5 行,聲明一個(gè)整型切片,切片中擁有多個(gè)整型數(shù)值。
- 第 8 行,將 numListEmpty 聲明為一個(gè)整型切片,本來(lái)會(huì)在{}中填充切片的初始化元素,這里沒(méi)有填充,所以切片是空的,但是此時(shí)的 numListEmpty 已經(jīng)被分配了內(nèi)存,只是還沒(méi)有元素。
- 第 11 行,切片均沒(méi)有任何元素,3 個(gè)切片輸出元素內(nèi)容均為空。
- 第 14 行,沒(méi)有對(duì)切片進(jìn)行任何操作,strList 和 numList 沒(méi)有指向任何數(shù)組或者其他切片。
- 第 17 行和第 18 行,聲明但未使用的切片的默認(rèn)值是 nil,strList 和 numList 也是 nil,所以和 nil 比較的結(jié)果是 true。
- 第 19 行,numListEmpty 已經(jīng)被分配到了內(nèi)存,但沒(méi)有元素,因此和 nil 比較時(shí)是 false。
切片是動(dòng)態(tài)結(jié)構(gòu),只能與 nil 判定相等,不能互相判定相等。聲明新的切片后,可以使用?append()?函數(shù)向切片中添加元素。
使用 make() 函數(shù)構(gòu)造切片
如果需要?jiǎng)討B(tài)地創(chuàng)建一個(gè)切片,可以使用 make() 內(nèi)建函數(shù),格式如下:
make( []Type, size, cap )
其中 Type 是指切片的元素類(lèi)型,size 指的是為這個(gè)類(lèi)型分配多少個(gè)元素,cap 為預(yù)分配的元素?cái)?shù)量,這個(gè)值設(shè)定后不影響 size,只是能提前分配空間,降低多次分配空間造成的性能問(wèn)題。
示例如下:
代碼輸出如下:
[0 0] [0 0]
2 2
其中 a 和 b 均是預(yù)分配 2 個(gè)元素的切片,只是 b 的內(nèi)部存儲(chǔ)空間已經(jīng)分配了 10 個(gè),但實(shí)際使用了 2 個(gè)元素。
容量不會(huì)影響當(dāng)前的元素個(gè)數(shù),因此 a 和 b 取 len 都是 2。
溫馨提示
使用 make() 函數(shù)生成的切片一定發(fā)生了內(nèi)存分配操作,但給定開(kāi)始與結(jié)束位置(包括切片復(fù)位)的切片只是將新的切片結(jié)構(gòu)指向已經(jīng)分配好的內(nèi)存區(qū)域,設(shè)定開(kāi)始與結(jié)束位置,不會(huì)發(fā)生內(nèi)存分配操作。
3.4?Go語(yǔ)言append()為切片添加元素
Go語(yǔ)言的內(nèi)建函數(shù) append() 可以為切片動(dòng)態(tài)添加元素,代碼如下所示:
不過(guò)需要注意的是,在使用 append() 函數(shù)為切片動(dòng)態(tài)添加元素時(shí),如果空間不足以容納足夠多的元素,切片就會(huì)進(jìn)行“擴(kuò)容”,此時(shí)新切片的長(zhǎng)度會(huì)發(fā)生改變。
切片在擴(kuò)容時(shí),容量的擴(kuò)展規(guī)律是按容量的 2 倍數(shù)進(jìn)行擴(kuò)充,例如 1、2、4、8、16……,代碼如下:
代碼輸出如下:
len: 1? cap: 1 pointer: 0xc0420080e8
len: 2? cap: 2 pointer: 0xc042008150
len: 3? cap: 4 pointer: 0xc04200e320
len: 4? cap: 4 pointer: 0xc04200e320
len: 5? cap: 8 pointer: 0xc04200c200
len: 6? cap: 8 pointer: 0xc04200c200
len: 7? cap: 8 pointer: 0xc04200c200
len: 8? cap: 8 pointer: 0xc04200c200
len: 9? cap: 16 pointer: 0xc042074000
len: 10? cap: 16 pointer: 0xc042074000
代碼說(shuō)明如下:
- 第 1 行,聲明一個(gè)整型切片。
- 第 4 行,循環(huán)向 numbers 切片中添加 10 個(gè)數(shù)。
- 第 5 行,打印輸出切片的長(zhǎng)度、容量和指針變化,使用函數(shù)?len() 查看切片擁有的元素個(gè)數(shù),使用函數(shù)?cap() 查看切片的容量情況。
通過(guò)查看代碼輸出,可以發(fā)現(xiàn)一個(gè)有意思的規(guī)律:切片長(zhǎng)度 len 并不等于切片的容量 cap。
往一個(gè)切片中不斷添加元素的過(guò)程,類(lèi)似于公司搬家,公司發(fā)展初期,資金緊張,人員很少,所以只需要很小的房間即可容納所有的員工,隨著業(yè)務(wù)的拓展和收入的增加就需要擴(kuò)充工位,但是辦公地的大小是固定的,無(wú)法改變,因此公司只能選擇搬家,每次搬家就需要將所有的人員轉(zhuǎn)移到新的辦公點(diǎn)。
- 員工和工位就是切片中的元素。
- 辦公地就是分配好的內(nèi)存。
- 搬家就是重新分配內(nèi)存。
- 無(wú)論搬多少次家,公司名稱(chēng)始終不會(huì)變,代表外部使用切片的變量名不會(huì)修改。
- 由于搬家后地址發(fā)生變化,因此內(nèi)存“地址”也會(huì)有修改。
除了在切片的尾部追加,我們還可以在切片的開(kāi)頭添加元素:
在切片開(kāi)頭添加元素一般都會(huì)導(dǎo)致內(nèi)存的重新分配,而且會(huì)導(dǎo)致已有元素全部被復(fù)制 1 次,因此,從切片的開(kāi)頭添加元素的性能要比從尾部追加元素的性能差很多。
因?yàn)?append 函數(shù)返回新切片的特性,所以切片也支持鏈?zhǔn)讲僮?#xff0c;我們可以將多個(gè) append 操作組合起來(lái),實(shí)現(xiàn)在切片中間插入元素:
每個(gè)添加操作中的第二個(gè) append 調(diào)用都會(huì)創(chuàng)建一個(gè)臨時(shí)切片,并將 a[i:] 的內(nèi)容復(fù)制到新創(chuàng)建的切片中,然后將臨時(shí)創(chuàng)建的切片再追加到 a[:i] 中。
3.5?Go語(yǔ)言copy():切片復(fù)制(切片拷貝)
Go語(yǔ)言的內(nèi)置函數(shù) copy() 可以將一個(gè)數(shù)組切片復(fù)制到另一個(gè)數(shù)組切片中,如果加入的兩個(gè)數(shù)組切片不一樣大,就會(huì)按照其中較小的那個(gè)數(shù)組切片的元素個(gè)數(shù)進(jìn)行復(fù)制。
copy() 函數(shù)的使用格式如下:
copy( destSlice, srcSlice []T) int
其中 srcSlice 為數(shù)據(jù)來(lái)源切片,destSlice 為復(fù)制的目標(biāo)(也就是將 srcSlice 復(fù)制到 destSlice),目標(biāo)切片必須分配過(guò)空間且足夠承載復(fù)制的元素個(gè)數(shù),并且來(lái)源和目標(biāo)的類(lèi)型必須一致,copy() 函數(shù)的返回值表示實(shí)際發(fā)生復(fù)制的元素個(gè)數(shù)。
下面的代碼展示了使用 copy() 函數(shù)將一個(gè)切片復(fù)制到另一個(gè)切片的過(guò)程:
雖然通過(guò)循環(huán)復(fù)制切片元素更直接,不過(guò)內(nèi)置的 copy() 函數(shù)使用起來(lái)更加方便,copy() 函數(shù)的第一個(gè)參數(shù)是要復(fù)制的目標(biāo) slice,第二個(gè)參數(shù)是源 slice,兩個(gè) slice 可以共享同一個(gè)底層數(shù)組,甚至有重疊也沒(méi)有問(wèn)題。
【示例】通過(guò)代碼演示對(duì)切片的引用和復(fù)制操作后對(duì)切片元素的影響。
代碼說(shuō)明如下:
- 第 8 行,定義元素總量為 1000。
- 第 11 行,預(yù)分配擁有 1000 個(gè)元素的整型切片,這個(gè)切片將作為原始數(shù)據(jù)。
- 第 14~16 行,將 srcData 填充 0~999 的整型值。
- 第 19 行,將 refData 引用 srcData,切片不會(huì)因?yàn)榈忍?hào)操作進(jìn)行元素的復(fù)制。
- 第 22 行,預(yù)分配與 srcData 等大(大小相等)、同類(lèi)型的切片 copyData。
- 第 24 行,使用 copy() 函數(shù)將原始數(shù)據(jù)復(fù)制到 copyData 切片空間中。
- 第 27 行,修改原始數(shù)據(jù)的第一個(gè)元素為 999。
- 第 30 行,引用數(shù)據(jù)的第一個(gè)元素將會(huì)發(fā)生變化。
- 第 33 行,打印復(fù)制數(shù)據(jù)的首位數(shù)據(jù),由于數(shù)據(jù)是復(fù)制的,因此不會(huì)發(fā)生變化。
- 第 36 行,將 srcData 的局部數(shù)據(jù)復(fù)制到 copyData 中。
- 第 38~40 行,打印復(fù)制局部數(shù)據(jù)后的 copyData 元素。
3.6?Go語(yǔ)言從切片中刪除元素
Go語(yǔ)言并沒(méi)有對(duì)刪除切片元素提供專(zhuān)用的語(yǔ)法或者接口,需要使用切片本身的特性來(lái)刪除元素,根據(jù)要?jiǎng)h除元素的位置有三種情況,分別是從開(kāi)頭位置刪除、從中間位置刪除和從尾部刪除,其中刪除切片尾部的元素速度最快。
從開(kāi)頭位置刪除
刪除開(kāi)頭的元素可以直接移動(dòng)數(shù)據(jù)指針:
也可以不移動(dòng)數(shù)據(jù)指針,但是將后面的數(shù)據(jù)向開(kāi)頭移動(dòng),可以用 append 原地完成(所謂原地完成是指在原有的切片數(shù)據(jù)對(duì)應(yīng)的內(nèi)存區(qū)間內(nèi)完成,不會(huì)導(dǎo)致內(nèi)存空間結(jié)構(gòu)的變化):
還可以用 copy() 函數(shù)來(lái)刪除開(kāi)頭的元素:
從中間位置刪除
對(duì)于刪除中間的元素,需要對(duì)剩余的元素進(jìn)行一次整體挪動(dòng),同樣可以用 append 或 copy 原地完成:
從尾部刪除
刪除開(kāi)頭的元素和刪除尾部的元素都可以認(rèn)為是刪除中間元素操作的特殊情況,下面來(lái)看一個(gè)示例。
【示例】刪除切片指定位置的元素。
代碼輸出結(jié)果:
[a b] [d e]
[a b d e]
代碼說(shuō)明如下:
- 第 1 行,聲明一個(gè)整型切片,保存含有從 a 到 e 的字符串。
- 第 4 行,為了演示和講解方便,使用 index 變量保存需要?jiǎng)h除的元素位置。
- 第 7 行,seq[:index] 表示的就是被刪除元素的前半部分,值為 [1 2],seq[index+1:] 表示的是被刪除元素的后半部分,值為?[4 5]。
- 第 10 行,使用 append() 函數(shù)將兩個(gè)切片連接起來(lái)。
- 第 12 行,輸出連接好的新切片,此時(shí),索引為 2 的元素已經(jīng)被刪除。
代碼的刪除過(guò)程可以使用下圖來(lái)描述。
圖:切片刪除元素的操作過(guò)程
Go語(yǔ)言中刪除切片元素的本質(zhì)是,以被刪除元素為分界點(diǎn),將前后兩個(gè)部分的內(nèi)存重新連接起來(lái)。
提示
連續(xù)容器的元素刪除無(wú)論在任何語(yǔ)言中,都要將刪除點(diǎn)前后的元素移動(dòng)到新的位置,隨著元素的增加,這個(gè)過(guò)程將會(huì)變得極為耗時(shí),因此,當(dāng)業(yè)務(wù)需要大量、頻繁地從一個(gè)切片中刪除元素時(shí),如果對(duì)性能要求較高的話(huà),就需要考慮更換其他的容器了(如雙鏈表等能快速?gòu)膭h除點(diǎn)刪除元素)。
3.7?Go語(yǔ)言range關(guān)鍵字:循環(huán)迭代切片
通過(guò)前面的學(xué)習(xí)我們了解到切片其實(shí)就是多個(gè)相同類(lèi)型元素的連續(xù)集合,既然切片是一個(gè)集合,那么我們就可以迭代其中的元素,Go語(yǔ)言有個(gè)特殊的關(guān)鍵字 range,它可以配合關(guān)鍵字 for 來(lái)迭代切片里的每一個(gè)元素,如下所示:
第 4 行中的 index 和 value 分別用來(lái)接收 range 關(guān)鍵字返回的切片中每個(gè)元素的索引和值,這里的 index 和 value 不是固定的,讀者也可以定義成其它的名字。
關(guān)于 for 的詳細(xì)使用我們將在下一章《Go語(yǔ)言流程控制》中為大家詳細(xì)介紹。
上面代碼的輸出結(jié)果為:
Index: 0 Value: 10
Index: 1 Value: 20
Index: 2 Value: 30
Index: 3 Value: 40
當(dāng)?shù)衅瑫r(shí),關(guān)鍵字 range 會(huì)返回兩個(gè)值,第一個(gè)值是當(dāng)前迭代到的索引位置,第二個(gè)值是該位置對(duì)應(yīng)元素值的一份副本,如下圖所示。
?
圖:使用 range 迭代切片會(huì)創(chuàng)建每個(gè)元素的副本
需要強(qiáng)調(diào)的是,range 返回的是每個(gè)元素的副本,而不是直接返回對(duì)該元素的引用,如下所示。
【示例 1】range 提供了每個(gè)元素的副本
輸出結(jié)果為:
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è)在迭代過(guò)程中根據(jù)切片依次賦值的新變量,所以 value 的地址總是相同的,要想獲取每個(gè)元素的地址,需要使用切片變量和索引值(例如上面代碼中的 &slice[index])。
如果不需要索引值,也可以使用下劃線_來(lái)忽略這個(gè)值,代碼如下所示。
【示例 2】使用空白標(biāo)識(shí)符(下劃線)來(lái)忽略索引值
輸出結(jié)果為:
Value: 10
Value: 20
Value: 30
Value: 40
關(guān)鍵字 range 總是會(huì)從切片頭部開(kāi)始迭代。如果想對(duì)迭代做更多的控制,則可以使用傳統(tǒng)的 for 循環(huán),代碼如下所示。
【示例 3】使用傳統(tǒng)的 for 循環(huán)對(duì)切片進(jìn)行迭代
輸出結(jié)果為:
Index: 2 Value: 30
Index: 3 Value: 40
在前面幾節(jié)的學(xué)習(xí)中我們了解了兩個(gè)特殊的內(nèi)置函數(shù) len() 和 cap(),可以用于處理數(shù)組、切片和通道,對(duì)于切片,函數(shù) len() 可以返回切片的長(zhǎng)度,函數(shù) cap() 可以返回切片的容量,在上面的示例中,使用到了函數(shù) len() 來(lái)控制循環(huán)迭代的次數(shù)。
當(dāng)然,range 關(guān)鍵字不僅僅可以用來(lái)遍歷切片,它還可以用來(lái)遍歷數(shù)組、字符串、map 或者通道等,這些我們將在后面的學(xué)習(xí)中詳細(xì)介紹。
3.8?Go語(yǔ)言多維切片簡(jiǎn)述
Go語(yǔ)言中同樣允許使用多維切片,聲明一個(gè)多維數(shù)組的語(yǔ)法格式如下:
var sliceName [][]...[]sliceType
其中,sliceName 為切片的名字,sliceType為切片的類(lèi)型,每個(gè)[ ]代表著一個(gè)維度,切片有幾個(gè)維度就需要幾個(gè)[ ]。
下面以二維切片為例,聲明一個(gè)二維切片并賦值,代碼如下所示。
上面的代碼也可以簡(jiǎn)寫(xiě)為下面的樣子。
上面的代碼中展示了一個(gè)包含兩個(gè)元素的外層切片,同時(shí)每個(gè)元素包又含一個(gè)內(nèi)層的整型切片,切片 slice 的值如下圖所示。
?
圖:整型切片的切片的值
通過(guò)上圖可以看到外層的切片包括兩個(gè)元素,每個(gè)元素都是一個(gè)切片,第一個(gè)元素中的切片使用單個(gè)整數(shù) 10 來(lái)初始化,第二個(gè)元素中的切片包括兩個(gè)整數(shù),即 100 和 200。
這種組合可以讓用戶(hù)創(chuàng)建非常復(fù)雜且強(qiáng)大的數(shù)據(jù)結(jié)構(gòu),前面介紹過(guò)的關(guān)于內(nèi)置函數(shù)?append()?的規(guī)則也可以應(yīng)用到組合后的切片上,如下所示。
【示例】組合切片的切片
Go語(yǔ)言里使用 append() 函數(shù)處理追加的方式很簡(jiǎn)明,先增長(zhǎng)切片,再將新的整型切片賦值給外層切片的第一個(gè)元素,當(dāng)上面代碼中的操作完成后,再將切片復(fù)制到外層切片的索引為 0 的元素,如下圖所示。
?
圖:append 操作之后外層切片索引為 0 的元素的布局
即便是這么簡(jiǎn)單的多維切片,操作時(shí)也會(huì)涉及眾多的布局和值,在函數(shù)間這樣傳遞數(shù)據(jù)結(jié)構(gòu)會(huì)很復(fù)雜,不過(guò)切片本身結(jié)構(gòu)很簡(jiǎn)單,可以用很小的成本在函數(shù)間傳遞。
3.9?Go語(yǔ)言map(Go語(yǔ)言映射)
Go語(yǔ)言中 map 是一種特殊的數(shù)據(jù)結(jié)構(gòu),一種元素對(duì)(pair)的無(wú)序集合,pair 對(duì)應(yīng)一個(gè) key(索引)和一個(gè) value(值),所以這個(gè)結(jié)構(gòu)也稱(chēng)為關(guān)聯(lián)數(shù)組或字典,這是一種能夠快速尋找值的理想結(jié)構(gòu),給定 key,就可以迅速找到對(duì)應(yīng)的 value。
map 這種數(shù)據(jù)結(jié)構(gòu)在其他編程語(yǔ)言中也稱(chēng)為字典(Python)、hash 和 HashTable 等。
map 概念
map 是引用類(lèi)型,可以使用如下方式聲明:
var mapname map[keytype]valuetype
其中:
- mapname 為 map 的變量名。
- keytype 為鍵類(lèi)型。
- valuetype 是鍵對(duì)應(yīng)的值類(lèi)型。
提示:[keytype] 和 valuetype 之間允許有空格。
在聲明的時(shí)候不需要知道 map 的長(zhǎng)度,因?yàn)?map 是可以動(dòng)態(tài)增長(zhǎng)的,未初始化的 map 的值是 nil,使用函數(shù) len() 可以獲取 map 中 pair 的數(shù)目。
【示例】
輸出結(jié)果:
Map literal at "one" is: 1
Map created at "key2" is: 3.14159
Map assigned at "two" is: 3
Map literal at "ten" is: 0
示例中 mapLit 演示了使用{key1: value1, key2: value2}的格式來(lái)初始化 map ,就像數(shù)組和結(jié)構(gòu)體一樣。
上面代碼中的 mapCreated 的創(chuàng)建方式mapCreated := make(map[string]float)等價(jià)于mapCreated := map[string]float{}?。
mapAssigned 是 mapList 的引用,對(duì) mapAssigned 的修改也會(huì)影響到 mapLit 的值。
注意:可以使用 make(),但不能使用 new() 來(lái)構(gòu)造 map,如果錯(cuò)誤的使用 new() 分配了一個(gè)引用對(duì)象,會(huì)獲得一個(gè)空引用的指針,相當(dāng)于聲明了一個(gè)未初始化的變量并且取了它的地址:
mapCreated := new(map[string]float)
接下來(lái)當(dāng)我們調(diào)用mapCreated["key1"] = 4.5的時(shí)候,編譯器會(huì)報(bào)錯(cuò):
invalid operation: mapCreated["key1"] (index of type *map[string]float).
map 容量
和數(shù)組不同,map 可以根據(jù)新增的 key-value 動(dòng)態(tài)的伸縮,因此它不存在固定長(zhǎng)度或者最大限制,但是也可以選擇標(biāo)明 map 的初始容量 capacity,格式如下:
make(map[keytype]valuetype, cap)
例如:
map2 := make(map[string]float, 100)
當(dāng) map 增長(zhǎng)到容量上限的時(shí)候,如果再增加新的 key-value,map 的大小會(huì)自動(dòng)加 1,所以出于性能的考慮,對(duì)于大的 map 或者會(huì)快速擴(kuò)張的 map,即使只是大概知道容量,也最好先標(biāo)明。
這里有一個(gè) map 的具體例子,即將音階和對(duì)應(yīng)的音頻映射起來(lái):
用切片作為 map 的值
既然一個(gè) key 只能對(duì)應(yīng)一個(gè) value,而 value 又是一個(gè)原始類(lèi)型,那么如果一個(gè) key 要對(duì)應(yīng)多個(gè)值怎么辦?例如,當(dāng)我們要處理 unix 機(jī)器上的所有進(jìn)程,以父進(jìn)程(pid 為整形)作為 key,所有的子進(jìn)程(以所有子進(jìn)程的 pid 組成的切片)作為 value。通過(guò)將 value 定義為 []int 類(lèi)型或者其他類(lèi)型的切片,就可以?xún)?yōu)雅的解決這個(gè)問(wèn)題,示例代碼如下所示:
3.10?Go語(yǔ)言遍歷map(訪問(wèn)map中的每一個(gè)鍵值對(duì))
map 的遍歷過(guò)程使用 for range 循環(huán)完成,代碼如下:
遍歷對(duì)于Go語(yǔ)言的很多對(duì)象來(lái)說(shuō)都是差不多的,直接使用 for range 語(yǔ)法即可,遍歷時(shí),可以同時(shí)獲得鍵和值,如只遍歷值,可以使用下面的形式:
將不需要的鍵使用_改為匿名變量形式。
只遍歷鍵時(shí),使用下面的形式:
無(wú)須將值改為匿名變量形式,忽略值即可。
注意:遍歷輸出元素的順序與填充順序無(wú)關(guān),不能期望 map 在遍歷時(shí)返回某種期望順序的結(jié)果。
如果需要特定順序的遍歷結(jié)果,正確的做法是先排序,代碼如下:
代碼輸出如下:
[brazil china route]
代碼說(shuō)明如下:
- 第 1 行,創(chuàng)建一個(gè) map 實(shí)例,鍵為字符串,值為整型。
- 第 4~6 行,將 3 個(gè)鍵值對(duì)寫(xiě)入 map 中。
- 第 9 行,聲明 sceneList 為字符串切片,以緩沖和排序 map 中的所有元素。
- 第 12 行,將 map 中元素的鍵遍歷出來(lái),并放入切片中。
- 第 17 行,對(duì) sceneList 字符串切片進(jìn)行排序,排序時(shí),sceneList 會(huì)被修改。
- 第 20 行,輸出排好序的 map 的鍵。
sort.Strings 的作用是對(duì)傳入的字符串切片進(jìn)行字符串字符的升序排列,排序接口的使用將在后面的章節(jié)中介紹。
3.11?Go語(yǔ)言map元素的刪除和清空
Go語(yǔ)言提供了一個(gè)內(nèi)置函數(shù) delete(),用于刪除容器內(nèi)的元素,下面我們簡(jiǎn)單介紹一下如何用 delete() 函數(shù)刪除 map 內(nèi)的元素。
使用 delete() 函數(shù)從 map 中刪除鍵值對(duì)
使用 delete() 內(nèi)建函數(shù)從 map 中刪除一組鍵值對(duì),delete() 函數(shù)的格式如下:
delete(map, 鍵)
其中 map 為要?jiǎng)h除的 map 實(shí)例,鍵為要?jiǎng)h除的 map 中鍵值對(duì)的鍵。
從 map 中刪除一組鍵值對(duì)可以通過(guò)下面的代碼來(lái)完成:
代碼輸出如下:
route 66
china 960
這個(gè)例子中使用 delete() 函數(shù)將 brazil 從 scene 這個(gè) map 中刪除了。
清空 map 中的所有元素
有意思的是,Go語(yǔ)言中并沒(méi)有為 map 提供任何清空所有元素的函數(shù)、方法,清空 map 的唯一辦法就是重新 make 一個(gè)新的 map,不用擔(dān)心垃圾回收的效率,Go語(yǔ)言中的并行垃圾回收效率比寫(xiě)一個(gè)清空函數(shù)要高效的多。
3.12?Go語(yǔ)言map的多鍵索引——多個(gè)數(shù)值條件可以同時(shí)查詢(xún)
3.13?Go語(yǔ)言sync.Map(在并發(fā)環(huán)境中使用的map)
Go語(yǔ)言中的 map 在并發(fā)情況下,只讀是線程安全的,同時(shí)讀寫(xiě)是線程不安全的。
下面來(lái)看下并發(fā)情況下讀寫(xiě) map 時(shí)會(huì)出現(xiàn)的問(wèn)題,代碼如下:
運(yùn)行代碼會(huì)報(bào)錯(cuò),輸出如下:
fatal error: concurrent map read and map write
錯(cuò)誤信息顯示,并發(fā)的 map 讀和 map 寫(xiě),也就是說(shuō)使用了兩個(gè)并發(fā)函數(shù)不斷地對(duì) map 進(jìn)行讀和寫(xiě)而發(fā)生了競(jìng)態(tài)問(wèn)題,map 內(nèi)部會(huì)對(duì)這種并發(fā)操作進(jìn)行檢查并提前發(fā)現(xiàn)。
需要并發(fā)讀寫(xiě)時(shí),一般的做法是加鎖,但這樣性能并不高,Go語(yǔ)言在 1.9 版本中提供了一種效率較高的并發(fā)安全的 sync.Map,sync.Map 和 map 不同,不是以語(yǔ)言原生形態(tài)提供,而是在 sync 包下的特殊結(jié)構(gòu)。
sync.Map 有以下特性:
- 無(wú)須初始化,直接聲明即可。
- sync.Map 不能使用 map 的方式進(jìn)行取值和設(shè)置等操作,而是使用 sync.Map 的方法進(jìn)行調(diào)用,Store 表示存儲(chǔ),Load 表示獲取,Delete 表示刪除。
- 使用 Range 配合一個(gè)回調(diào)函數(shù)進(jìn)行遍歷操作,通過(guò)回調(diào)函數(shù)返回內(nèi)部遍歷出來(lái)的值,Range 參數(shù)中回調(diào)函數(shù)的返回值在需要繼續(xù)迭代遍歷時(shí),返回 true,終止迭代遍歷時(shí),返回 false。
并發(fā)安全的 sync.Map?演示代碼如下:
代碼輸出如下:
100 true
iterate: egypt 200
iterate: greece 97
代碼說(shuō)明如下:
- 第 10 行,聲明 scene,類(lèi)型為 sync.Map,注意,sync.Map 不能使用 make 創(chuàng)建。
- 第 13~15 行,將一系列鍵值對(duì)保存到 sync.Map 中,sync.Map 將鍵和值以 interface{} 類(lèi)型進(jìn)行保存。
- 第 18 行,提供一個(gè) sync.Map 的鍵給 scene.Load() 方法后將查詢(xún)到鍵對(duì)應(yīng)的值返回。
- 第 21 行,sync.Map 的 Delete 可以使用指定的鍵將對(duì)應(yīng)的鍵值對(duì)刪除。
- 第 24 行,Range() 方法可以遍歷 sync.Map,遍歷需要提供一個(gè)匿名函數(shù),參數(shù)為 k、v,類(lèi)型為 interface{},每次 Range() 在遍歷一個(gè)元素時(shí),都會(huì)調(diào)用這個(gè)匿名函數(shù)把結(jié)果返回。
sync.Map 沒(méi)有提供獲取 map 數(shù)量的方法,替代方法是在獲取 sync.Map 時(shí)遍歷自行計(jì)算數(shù)量,sync.Map 為了保證并發(fā)安全有一些性能損失,因此在非并發(fā)情況下,使用 map 相比使用 sync.Map 會(huì)有更好的性能。
3.14?Go語(yǔ)言list(列表)
列表是一種非連續(xù)的存儲(chǔ)容器,由多個(gè)節(jié)點(diǎn)組成,節(jié)點(diǎn)通過(guò)一些變量記錄彼此之間的關(guān)系,列表有多種實(shí)現(xiàn)方法,如單鏈表、雙鏈表等。
列表的原理可以這樣理解:假設(shè) A、B、C 三個(gè)人都有電話(huà)號(hào)碼,如果 A 把號(hào)碼告訴給 B,B 把號(hào)碼告訴給 C,這個(gè)過(guò)程就建立了一個(gè)單鏈表結(jié)構(gòu),如下圖所示。
?
圖:三人單向通知電話(huà)號(hào)碼形成單鏈表結(jié)構(gòu)
如果在這個(gè)基礎(chǔ)上,再?gòu)?C 開(kāi)始將自己的號(hào)碼告訴給自己所知道號(hào)碼的主人,這樣就形成了雙鏈表結(jié)構(gòu),如下圖所示。
?
圖:三人相互通知電話(huà)號(hào)碼形成雙鏈表結(jié)構(gòu)
那么如果需要獲得所有人的號(hào)碼,只需要從 A 或者 C 開(kāi)始,要求他們將自己的號(hào)碼發(fā)出來(lái),然后再通知下一個(gè)人如此循環(huán),這樣就構(gòu)成了一個(gè)列表遍歷的過(guò)程。
如果 B 換號(hào)碼了,他需要通知 A 和 C,將自己的號(hào)碼移除,這個(gè)過(guò)程就是列表元素的刪除操作,如下圖所示。
?
圖:從雙鏈表中刪除一人的電話(huà)號(hào)碼
在Go語(yǔ)言中,列表使用 container/list 包來(lái)實(shí)現(xiàn),內(nèi)部的實(shí)現(xiàn)原理是雙鏈表,列表能夠高效地進(jìn)行任意位置的元素插入和刪除操作。
初始化列表
list 的初始化有兩種方法:分別是使用 New() 函數(shù)和 var 關(guān)鍵字聲明,兩種方法的初始化效果都是一致的。
1) 通過(guò) container/list 包的 New() 函數(shù)初始化 list
變量名 := list.New()
2) 通過(guò) var 關(guān)鍵字聲明初始化 list
var 變量名 list.List
列表與切片和 map 不同的是,列表并沒(méi)有具體元素類(lèi)型的限制,因此,列表的元素可以是任意類(lèi)型,這既帶來(lái)了便利,也引來(lái)一些問(wèn)題,例如給列表中放入了一個(gè) interface{} 類(lèi)型的值,取出值后,如果要將 interface{} 轉(zhuǎn)換為其他類(lèi)型將會(huì)發(fā)生宕機(jī)。
在列表中插入元素
雙鏈表支持從隊(duì)列前方或后方插入元素,分別對(duì)應(yīng)的方法是 PushFront 和 PushBack。
提示
這兩個(gè)方法都會(huì)返回一個(gè) *list.Element 結(jié)構(gòu),如果在以后的使用中需要?jiǎng)h除插入的元素,則只能通過(guò) *list.Element 配合 Remove() 方法進(jìn)行刪除,這種方法可以讓刪除更加效率化,同時(shí)也是雙鏈表特性之一。
下面代碼展示如何給 list 添加元素:
代碼說(shuō)明如下:
- 第 1 行,創(chuàng)建一個(gè)列表實(shí)例。
- 第 3 行,將 fist 字符串插入到列表的尾部,此時(shí)列表是空的,插入后只有一個(gè)元素。
- 第 4 行,將數(shù)值 67 放入列表,此時(shí),列表中已經(jīng)存在 fist 元素,67 這個(gè)元素將被放在 fist 的前面。
列表插入元素的方法如下表所示。
?
| InsertAfter(v interface {}, mark * Element) * Element | 在 mark 點(diǎn)之后插入元素,mark 點(diǎn)由其他插入函數(shù)提供 |
| InsertBefore(v interface?{}, mark * Element) *Element | 在 mark 點(diǎn)之前插入元素,mark 點(diǎn)由其他插入函數(shù)提供 |
| PushBackList(other *List) | 添加 other 列表元素到尾部 |
| PushFrontList(other *List) | 添加 other 列表元素到頭部 |
從列表中刪除元素
列表插入函數(shù)的返回值會(huì)提供一個(gè) *list.Element 結(jié)構(gòu),這個(gè)結(jié)構(gòu)記錄著列表元素的值以及與其他節(jié)點(diǎn)之間的關(guān)系等信息,從列表中刪除元素時(shí),需要用到這個(gè)結(jié)構(gòu)進(jìn)行快速刪除。
列表操作元素:
代碼說(shuō)明如下:
第 6 行,創(chuàng)建列表實(shí)例。
第 9 行,將字符串?canon 插入到列表的尾部。
第 12 行,將數(shù)值?67 添加到列表的頭部。
第 15 行,將字符串?fist 插入到列表的尾部,并將這個(gè)元素的內(nèi)部結(jié)構(gòu)保存到 element 變量中。
第 18 行,使用 element 變量,在 element 的位置后面插入 high 字符串。
第 21 行,使用 element 變量,在 element 的位置前面插入 noon 字符串。
第 24 行,移除 element 變量對(duì)應(yīng)的元素。
下表中展示了每次操作后列表的實(shí)際元素情況。
?
| l.PushBack("canon") | canon |
| l.PushFront(67) | 67,?canon |
| element := l.PushBack("fist") | 67, canon, fist |
| l.InsertAfter("high", element) | 67, canon, fist, high |
| l.InsertBefore("noon", element) | 67, canon, noon, fist, high |
| l.Remove(element) | 67, canon, noon, high |
遍歷列表——訪問(wèn)列表的每一個(gè)元素
遍歷雙鏈表需要配合 Front() 函數(shù)獲取頭元素,遍歷時(shí)只要元素不為空就可以繼續(xù)進(jìn)行,每一次遍歷都會(huì)調(diào)用元素的 Next() 函數(shù),代碼如下所示。
代碼輸出如下:
67
canon
代碼說(shuō)明如下:
- 第 1 行,創(chuàng)建一個(gè)列表實(shí)例。
- 第 4 行,將 canon 放入列表尾部。
- 第 7 行,在隊(duì)列頭部放入 67。
- 第 9 行,使用 for 語(yǔ)句進(jìn)行遍歷,其中 i:=l.Front() 表示初始賦值,只會(huì)在一開(kāi)始執(zhí)行一次,每次循環(huán)會(huì)進(jìn)行一次 i != nil 語(yǔ)句判斷,如果返回 false,表示退出循環(huán),反之則會(huì)執(zhí)行 i = i.Next()。
- 第 10 行,使用遍歷返回的 *list.Element 的 Value 成員取得放入列表時(shí)的原值。
3.15?Go語(yǔ)言nil:空值/零值
在Go語(yǔ)言中,布爾類(lèi)型的零值(初始值)為 false,數(shù)值類(lèi)型的零值為 0,字符串類(lèi)型的零值為空字符串"",而指針、切片、映射、通道、函數(shù)和接口的零值則是 nil。
nil 是Go語(yǔ)言中一個(gè)預(yù)定義好的標(biāo)識(shí)符,有過(guò)其他編程語(yǔ)言開(kāi)發(fā)經(jīng)驗(yàn)的開(kāi)發(fā)者也許會(huì)把 nil 看作其他語(yǔ)言中的 null(NULL),其實(shí)這并不是完全正確的,因?yàn)镚o語(yǔ)言中的 nil 和其他語(yǔ)言中的 null 有很多不同點(diǎn)。
下面通過(guò)幾個(gè)方面來(lái)介紹一下Go語(yǔ)言中 nil。
nil 標(biāo)識(shí)符是不能比較的
運(yùn)行結(jié)果如下所示:
PS D:\code> go run .\main.go
# command-line-arguments
.\main.go:8:21: invalid operation: nil == nil (operator == not defined on nil)
這點(diǎn)和 python 等動(dòng)態(tài)語(yǔ)言是不同的,在 python 中,兩個(gè) None 值永遠(yuǎn)相等。
>>> None == None
True
從上面的運(yùn)行結(jié)果不難看出,==對(duì)于 nil 來(lái)說(shuō)是一種未定義的操作。
nil 不是關(guān)鍵字或保留字
nil 并不是Go語(yǔ)言的關(guān)鍵字或者保留字,也就是說(shuō)我們可以定義一個(gè)名稱(chēng)為 nil 的變量,比如下面這樣:
var nil = errors.New("my god")
雖然上面的聲明語(yǔ)句可以通過(guò)編譯,但是并不提倡這么做。
nil 沒(méi)有默認(rèn)類(lèi)型
運(yùn)行結(jié)果如下所示:
PS D:\code> go run .\main.go
# command-line-arguments
.\main.go:9:10: use of untyped nil
不同類(lèi)型 nil 的指針是一樣的
運(yùn)行結(jié)果如下所示:
PS D:\code> go run .\main.go
0x0
0x0
通過(guò)運(yùn)行結(jié)果可以看出 arr 和 num 的指針都是 0x0。
不同類(lèi)型的 nil 是不能比較的
運(yùn)行結(jié)果如下所示:
PS D:\code> go run .\main.go
# command-line-arguments
.\main.go:10:20: invalid operation: arr == ptr (mismatched types []int and *int)
兩個(gè)相同類(lèi)型的 nil 值也可能無(wú)法比較
在Go語(yǔ)言中 map、slice 和 function 類(lèi)型的 nil 值不能比較,比較兩個(gè)無(wú)法比較類(lèi)型的值是非法的,下面的語(yǔ)句無(wú)法編譯。
運(yùn)行結(jié)果如下所示:
PS D:\code> go run .\main.go
# command-line-arguments
.\main.go:10:19: invalid operation: s1 == s2 (slice can only be compared to nil)
通過(guò)上面的錯(cuò)誤提示可以看出,能夠?qū)⑸鲜霾豢杀容^類(lèi)型的空值直接與 nil 標(biāo)識(shí)符進(jìn)行比較,如下所示:
運(yùn)行結(jié)果如下所示:
PS D:\code> go run .\main.go
true
nil 是 map、slice、pointer、channel、func、interface 的零值
運(yùn)行結(jié)果如下所示:
PS D:\code> go run .\main.go
map[int]string(nil)
(*int)(nil)
(chan int)(nil)
[]int(nil)
(func())(nil)
<nil>
零值是Go語(yǔ)言中變量在聲明之后但是未初始化被賦予的該類(lèi)型的一個(gè)默認(rèn)值。
不同類(lèi)型的 nil 值占用的內(nèi)存大小可能是不一樣的
一個(gè)類(lèi)型的所有的值的內(nèi)存布局都是一樣的,nil 也不例外,nil 的大小與同類(lèi)型中的非 nil 類(lèi)型的大小是一樣的。但是不同類(lèi)型的 nil 值的大小可能不同。
運(yùn)行結(jié)果如下所示:
PS D:\code> go run .\main.go
8
24
8
8
8
16
具體的大小取決于編譯器和架構(gòu),上面打印的結(jié)果是在 64 位架構(gòu)和標(biāo)準(zhǔn)編譯器下完成的,對(duì)應(yīng) 32 位的架構(gòu)的,打印的大小將減半。
3.16?Go語(yǔ)言make和new關(guān)鍵字的區(qū)別及實(shí)現(xiàn)原理
?
?
總結(jié)
以上是生活随笔為你收集整理的第03章 Go语言容器(container)的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: [小白教程]动态调试工具Ollydbg的
- 下一篇: 凡人修仙赚