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