Go第三篇之大话容器
Go語言數(shù)組
數(shù)組(Array)是一段固定長度的連續(xù)內(nèi)存區(qū)域
在 Go 語言中,數(shù)組從聲明時就確定,使用時可以修改數(shù)組成員,但是數(shù)組大小不可變化
Go 語言數(shù)組的聲明
數(shù)組的寫法如下:
var 數(shù)組變量名 [元素數(shù)量]T
其中:
- 數(shù)組變量名:數(shù)組聲明及使用時的變量名。
- 元素數(shù)量:數(shù)組的元素數(shù)量??梢允且粋€表達式,但最終通過編譯期計算的結(jié)果必須是整型數(shù)值。也就是說,元素數(shù)量不能含有到運行時才能確認大小的數(shù)值。
- T 可以是任意基本類型,包括 T 為數(shù)組本身。但類型為數(shù)組本身時,可以實現(xiàn)多維數(shù)組。
下面是一段數(shù)組的演示例子:
var team [3]string team[0] = "hammer" team[1] = "soldier" team[2] = "mum" fmt.Println(team)輸出結(jié)果:
[hammer soldier mum]代碼說明如下:
- 第 1 行,將 team 聲明為包含 3 個元素的字符串?dāng)?shù)組。
- 第 2~4 行,為 team 的元素賦值。
Go語言數(shù)組的初始化
數(shù)組可以在聲明時使用初始化列表進行元素設(shè)置,參考下面的代碼:
var team = [3]string{"hammer", "soldier", "mum"}這種方式編寫時,需要保證大括號后面的元素數(shù)量與數(shù)組的大小一致。但一般情況下,這個過程可以交給編譯器,讓編譯器在編譯時,根據(jù)元素個數(shù)確定數(shù)組大小。
var team = [...]string{"hammer", "soldier", "mum"}...表示讓編譯器確定數(shù)組大小。上面例子中,編譯器會自動為這個數(shù)組設(shè)置元素個數(shù)為 3。
遍歷數(shù)組(訪問每一個數(shù)組元素)
遍歷數(shù)組也和遍歷切片類似,看下面代碼:
var team [3]string team[0] = "hammer" team[1] = "soldier" team[2] = "mum" for k, v := range team { fmt.Println(k, v) }代碼輸出結(jié)果:
hammer soldier mum代碼說明如下:
- 第 6 行,使用 for 循環(huán),遍歷 team 數(shù)組,遍歷出的鍵 k 為數(shù)組的索引,值 v 為數(shù)組的每個元素值。
- 第 7 行,將每個鍵值打印出來。
Go語言切片
切片(Slice)是一個擁有相同類型元素的可變長度的序列。Go 語言切片的內(nèi)部結(jié)構(gòu)包含地址、大小和容量。切片一般用于快速地操作一塊數(shù)據(jù)集合。如果將數(shù)據(jù)集合比作切糕的話,切片就是你要的“那一塊”。切的過程包含從哪里開始(這個就是切片的地址)及切多大(這個就是切片的大小)。容量可以理解為裝切片的口袋大小,如下圖所示。
圖:切片結(jié)構(gòu)和內(nèi)存分配
從數(shù)組或切片生成新的切片
切片默認指向一段連續(xù)內(nèi)存區(qū)域,可以是數(shù)組,也可以是切片本身。
從連續(xù)內(nèi)存區(qū)域生成切片是常見的操作,格式如下:
slice [開始位置:結(jié)束位置]
- slice 表示目標(biāo)切片對象。
- 開始位置對應(yīng)目標(biāo)切片對象的索引。
- 結(jié)束位置對應(yīng)目標(biāo)切片的結(jié)束索引。
從數(shù)組生成切片,代碼如下:
var a = [3]int{1, 2, 3} fmt.Println(a, a[1:2])a 是一個擁有 3 個整型元素的數(shù)組,被初始化數(shù)值 1 到 3。使用 a[1:2] 可以生成一個新的切片。代碼運行結(jié)果如下:
[1 2 3] ?[2]
[2] 就是 a[1:2] 切片操作的結(jié)果。
從數(shù)組或切片生成新的切片擁有如下特性:
- 取出的元素數(shù)量為:結(jié)束位置-開始位置。
- 取出元素不包含結(jié)束位置對應(yīng)的索引,切片最后一個元素使用 slice[len(slice)] 獲取。
- 當(dāng)缺省開始位置時,表示從連續(xù)區(qū)域開頭到結(jié)束位置。
- 當(dāng)缺省結(jié)束位置時,表示從開始位置到整個連續(xù)區(qū)域末尾。
- 兩者同時缺省時,與切片本身等效。
- 兩者同時為0時,等效于空切片,一般用于切片復(fù)位。
根據(jù)索引位置取切片 slice 元素值時,取值范圍是(0~len(slice)-1),超界會報運行時錯誤。生成切片時,結(jié)束位置可以填寫 len(slice) 但不會報錯。
下面在具體的例子中熟悉切片的特性。
1) 從指定范圍中生成切片
切片和數(shù)組密不可分。如果將數(shù)組理解為一棟辦公樓,那么切片就是把不同的連續(xù)樓層出租給使用者。出租的過程需要選擇開始樓層和結(jié)束樓層,這個過程就會生成切片。示例代碼如下:
var highRiseBuilding [30]int for i := 0; i < 30; i++ { highRiseBuilding[i] = i + 1 } // 區(qū)間 fmt.Println(highRiseBuilding[10:15]) // 中間到尾部的所有元素 fmt.Println(highRiseBuilding[20:]) // 開頭到中間的所有元素 fmt.Println(highRiseBuilding[:2])代碼輸出如下:
[11 12 13 14 15] [21 22 23 24 25 26 27 28 29 30] [1 2] 代碼中構(gòu)建了一個 30 層的高層建筑。數(shù)組的元素值從 1 到 30,分別代表不同的獨立樓層。輸出的結(jié)果是不同租售方案。
代碼說明如下:
- 第 8 行,嘗試出租一個區(qū)間樓層。
- 第 11 行,出租 20 層以上。
- 第 14 行,出租 2 層以下,一般是商用鋪面。
切片有點像C語言里的指針。指針可以做運算,但代價是內(nèi)存操作越界。切片在指針的基礎(chǔ)上增加了大小,約束了切片對應(yīng)的內(nèi)存區(qū)域,切片使用中無法對切片內(nèi)部的地址和大小進行手動調(diào)整,因此切片比指針更安全、強大。
2) 表示原有的切片
生成切片的格式中,當(dāng)開始和結(jié)束都范圍都被忽略,則生成的切片將表示和原切片一致的切片,并且生成的切片與原切片在數(shù)據(jù)內(nèi)容上是一致的,代碼如下:
a := []int{1, 2, 3} fmt.Println(a[:])a 是一個擁有 3 個元素的切片。將 a 切片使用 a[:] 進行操作后,得到的切片與 a 切片一致,代碼輸出如下:
[1 2 3]3) 重置切片,清空擁有的元素
把切片的開始和結(jié)束位置都設(shè)為 0 時,生成的切片將變空,代碼如下:
a := []int{1, 2, 3} fmt.Println(a[0:0])代碼輸出如下:
[]
直接聲明新的切片
除了可以從原有的數(shù)組或者切片中生成切片,你也可以聲明一個新的切片。每一種類型都可以擁有其切片類型,表示多個類型元素的連續(xù)集合。因此切片類型也可以被聲明。切片類型聲明格式如下:
var name []T
- name 表示切片類型的變量名。
- T 表示切片類型對應(yīng)的元素類型。
下面代碼展示了切片聲明的使用過程:
// 聲明字符串切片 var strList []string // 聲明整型切片 var numList []int // 聲明一個空切片 var numListEmpty = []int{} // 輸出3個切片 fmt.Println(strList, numList, numListEmpty) // 輸出3個切片大小 fmt.Println(len(strList), len(numList), len(numListEmpty)) // 切片判定空的結(jié)果 fmt.Println(strList == nil) fmt.Println(numList == nil) fmt.Println(numListEmpty == nil)代碼輸出結(jié)果:
[] [] []
0 0 0
true
true
false
代碼說明如下:
- 第 2 行,聲明一個字符串切片,切片中擁有多個字符串。
- 第 5 行,聲明一個整型切片,切片中擁有多個整型數(shù)值。
- 第 8 行,將 numListEmpty 聲明為一個整型切片。本來會在{}中填充切片的初始化元素,這里沒有填充,所以切片是空的。但此時 numListEmpty 已經(jīng)被分配了內(nèi)存,但沒有元素。
- 第 11 行,切片均沒有任何元素,3 個切片輸出元素內(nèi)容均為空。
- 第 14 行,沒有對切片進行任何操作,strList 和 numList 沒有指向任何數(shù)組或者其他切片。
- 第 17 行和第 18 行,聲明但未使用的切片的默認值是 nil。strList 和 numList 也是 nil,所以和 nil 比較的結(jié)果是 true。
- 第 19 行,numListEmpty 已經(jīng)被分配到了內(nèi)存,但沒有元素,因此和 nil 比較時是 false。
切片是動態(tài)結(jié)構(gòu),只能與nil判定相等,不能互相判等時。
聲明新的切片后,可以使用 append() 函數(shù)來添加元素。
使用 make() 函數(shù)構(gòu)造切片
如果需要動態(tài)地創(chuàng)建一個切片,可以使用 make() 內(nèi)建函數(shù),格式如下:
make( []T, size, cap )
- T:切片的元素類型。
- size:就是為這個類型分配多少個元素。
- cap:預(yù)分配的元素數(shù)量,這個值設(shè)定后不影響 size,只是能提前分配空間,降低多次分配空間造成的性能問題。
示例如下:
代碼輸出如下:
[0 0] [0 0]
2 2
a 和 b 均是預(yù)分配 2 個元素的切片,只是 b 的內(nèi)部存儲空間已經(jīng)分配了 10 個,但實際使用了 2 個元素。
容量不會影響當(dāng)前的元素個數(shù),因此 a 和 b 取 len 都是 2。
溫馨提示
使用 make() 函數(shù)生成的切片一定發(fā)生了內(nèi)存分配操作。但給定開始與結(jié)束位置(包括切片復(fù)位)的切片只是將新的切片結(jié)構(gòu)指向已經(jīng)分配好的內(nèi)存區(qū)域,設(shè)定開始與結(jié)束位置,不會發(fā)生內(nèi)存分配操作。
切片不一定必須經(jīng)過 make() 函數(shù)才能使用。生成切片、聲明后使用 append() 函數(shù)均可以正常使用切片。
使用append()為切片添加元素
Go 語言的內(nèi)建函數(shù) append() 可以為切片動態(tài)添加元素。每個切片會指向一片內(nèi)存空間,這片空間能容納一定數(shù)量的元素。當(dāng)空間不能容納足夠多的元素時,切片就會進行“擴容”?!皵U容”操作往往發(fā)生在 append() 函數(shù)調(diào)用時。
切片在擴容時,容量的擴展規(guī)律按容量的 2 倍數(shù)擴充,例如 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 行,循環(huán)向 numbers 切片添加10個數(shù)。
- 第 5 行中,打印輸出切片的長度、容量和指針變化。使用 len() 函數(shù)查看切片擁有的元素個數(shù),使用 cap() 函數(shù)查看切片的容量情況。
通過查看代碼輸出,有一個有意思的規(guī)律:len() 函數(shù)并不等于 cap。
往一個切片中不斷添加元素的過程,類似于公司搬家。公司發(fā)展初期,資金緊張,人員很少,所以只需要很小的房間即可容納所有的員工。隨著業(yè)務(wù)的拓展和收入的增加就需要擴充工位,但是辦公地的大小是固定的,無法改變。因此公司選擇搬家,每次搬家就需要將所有的人員轉(zhuǎn)移到新的辦公點。
- 員工和工位就是切片中的元素。
- 辦公地就是分配好的內(nèi)存。
- 搬家就是重新分配內(nèi)存。
- 無論搬多少次家,公司名稱始終不會變,代表外部使用切片的變量名不會修改。
- 因為搬家后地址發(fā)生變化,因此內(nèi)存“地址”也會有修改。
append() 函數(shù)除了添加一個元素外,也可以一次性添加很多元素
代碼輸出如下:
[OldDriver Ice Sniper Monk Pig Flyingcake Chicken]
代碼說明如下:
- 第 1 行,聲明一個字符串切片。
- 第 4 行,往切片中添加一個元素。
- 第 7 行,使用 append() 函數(shù)向切片中添加多個元素。
- 第 10 行,聲明另外一個字符串切片
- 第 11 行,在team后面加上了...,表示將 team 整個添加到 car 的后面。
Go語言切片復(fù)制
使用 Go 語言內(nèi)建的 copy() 函數(shù),可以迅速地將一個切片的數(shù)據(jù)復(fù)制到另外一個切片空間中,copy() 函數(shù)的使用格式如下:
copy( destSlice, srcSlice []T) int
- srcSlice 為數(shù)據(jù)來源切片。
- destSlice 為復(fù)制的目標(biāo)。目標(biāo)切片必須分配過空間且足夠承載復(fù)制的元素個數(shù)。來源和目標(biāo)的類型一致,copy 的返回值表示實際發(fā)生復(fù)制的元素個數(shù)。
下面的代碼將演示對切片的引用和復(fù)制操作后對切片元素的影響
代碼說明如下:
- 第 8 行,定義元素總量為 1000。
- 第 11 行,預(yù)分配擁有 1000 個元素的整型切片,這個切片將作為原始數(shù)據(jù)。
- 第 14~16 行,將 srcData 填充 0~999 的整型值。
- 第 19 行,將 refData 引用 srcData,切片不會因為等號操作進行元素的復(fù)制。
- 第 22 行,預(yù)分配與 srcData 等大(大小相等)、同類型的切片 copyData。
- 第 24 行,使用 copy() 函數(shù)將原始數(shù)據(jù)復(fù)制到 copyData 切片空間中。
- 第 27 行,修改原始數(shù)據(jù)的第一個元素為 999。
- 第 30 行,引用數(shù)據(jù)的第一個元素將會發(fā)生變化。
- 第 33 行,打印復(fù)制數(shù)據(jù)的首位數(shù)據(jù),由于數(shù)據(jù)是復(fù)制的,因此不會發(fā)生變化。
- 第 36 行,將 srcData 的局部數(shù)據(jù)復(fù)制到 copyData 中。
- 第 38~40 行,打印復(fù)制局部數(shù)據(jù)后的 copyData 元素。
Go語言從切片中刪除元素
Go 語言并沒有對刪除切片元素提供專用的語法或者接口,需要使用切片本身的特性來刪除元素。示例代碼如下
seq := []string{"a", "b", "c", "d", "e"} // 指定刪除位置 index := 2 // 查看刪除位置之前的元素和之后的元素 fmt.Println(seq[:index], seq[index+1:]) // 將刪除點前后的元素連接起來 seq = append(seq[:index], seq[index+1:]...) fmt.Println(seq)代碼輸出結(jié)果:
[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() 函數(shù)將兩個切片連接起來。
- 第 12 行,輸出連接好的新切片。此時,索引為 2 的元素已經(jīng)被刪除。
代碼的刪除過程可以使用下圖來描述。
圖:切片刪除元素的操作過程
Go 語言中切片刪除元素的本質(zhì)是:以被刪除元素為分界點,將前后兩個部分的內(nèi)存重新連接起來。
提示
Go 語言中切片元素的刪除過程并沒有提供任何的語法糖或者方法封裝,無論是初學(xué)者學(xué)習(xí),還是實際使用都是極為麻煩的。
連續(xù)容器的元素刪除無論是在任何語言中,都要將刪除點前后的元素移動到新的位置。隨著元素的增加,這個過程將會變得極為耗時。因此,當(dāng)業(yè)務(wù)需要大量、頻繁地從一個切片中刪除元素時,如果對性能要求較高,就需要反思是否需要更換其他的容器(如雙鏈表等能快速從刪除點刪除元素)
Go語言map(映射)
在業(yè)務(wù)和算法中需要使用任意類型的關(guān)聯(lián)關(guān)系時,就需要使用到映射,如學(xué)號和學(xué)生的對應(yīng)、名字與檔案的對應(yīng)等。
Go 語言提供的映射關(guān)系容器為 map,map使用散列表(hash)實現(xiàn)。
提示
大多數(shù)語言中映射關(guān)系容器使用兩種算法:散列表和平衡樹。
散列表可以簡單描述為一個數(shù)組(俗稱“桶”),數(shù)組的每個元素是一個列表。根據(jù)散列函數(shù)獲得每個元素的特征值,將特征值作為映射的鍵。如果特征值重復(fù),表示元素發(fā)生碰撞。碰撞的元素將被放在同一個特征值的列表中進行保存。散列表查找復(fù)雜度為 O(1),和數(shù)組一致。最壞的情況為 O(n),n 為元素總數(shù)。散列需要盡量避免元素碰撞以提高查找效率,這樣就需要對“桶”進行擴容,每次擴容,元素需要重新放入桶中,較為耗時。
平衡樹類似于有父子關(guān)系的一棵數(shù)據(jù)樹,每個元素在放入樹時,都要與一些節(jié)點進行比較。平衡樹的查找復(fù)雜度始終為 O(log n)。
添加關(guān)聯(lián)到 map 并訪問關(guān)聯(lián)和數(shù)據(jù)
Go 語言中 map 的定義是這樣的:
map[KeyType]ValueType
- KeyType為鍵類型。
- ValueType是鍵對應(yīng)的值類型。
一個 map 里,符合 KeyType 和 ValueType 的映射總是成對出現(xiàn)。
下面代碼展示了 map 的基本使用環(huán)境
代碼輸出如下:
66
0
代碼說明如下:
- 第 1 行 map 是一個內(nèi)部實現(xiàn)的類型,使用時,需要手動使用 make 創(chuàng)建。如果不創(chuàng)建使用 map 類型,會觸發(fā)宕機錯誤。
- 第 3 行向 map 中加入映射關(guān)系。寫法與使用數(shù)組一樣,key 可以使用除函數(shù)以外的任意類型。
- 第 5 行查找 map 中的值。
- 第 7 行中,嘗試查找一個不存在的鍵,那么返回的將是 ValueType 的默認值。
某些情況下,需要明確知道查詢中某個鍵是否在 map 中存在,可以使用一種特殊的寫法來實現(xiàn),看下面的代碼:
在默認獲取鍵值的基礎(chǔ)上,多取了一個變量 ok,可以判斷鍵 route 是否存在于 map 中。
map 還有一種在聲明時填充內(nèi)容的方式,代碼如下:
例子中并沒有使用 make,而是使用大括號進行內(nèi)容定義,就像 JSON 格式一樣,冒號的左邊是 key,右邊是值,鍵值對之間使用逗號分隔。
Go語言遍歷map
map 的遍歷過程使用 for range 循環(huán)完成,代碼如下
scene := make(map[string]int) scene["route"] = 66 scene["brazil"] = 4 scene["china"] = 960 for k, v := range scene { fmt.Println(k, v) }遍歷對于 Go 語言的很多對象來說都是差不多的,直接使用 for range 語法。遍歷時,可以同時獲得鍵和值。如只遍歷值,可以使用下面的形式:
將不需要的鍵改為匿名變量形式。
只遍歷鍵時,使用下面的形式:
無須將值改為匿名變量形式,忽略值即可。
注意:遍歷輸出元素的順序與填充順序無關(guān)。不能期望 map 在遍歷時返回某種期望順序的結(jié)果。
如果需要特定順序的遍歷結(jié)果,正確的做法是排序,代碼如下
代碼輸出如下:
[brazil china route]
代碼說明如下:
- 第 1 行,創(chuàng)建一個 map 實例,鍵為字符串,值為整型。
- 第 4~6 行,將 3 個鍵值對寫入 map 中。
- 第 9 行,聲明 sceneList 為字符串切片,以緩沖和排序 map 中的所有元素。
- 第 12 行,將 map 中元素的鍵遍歷出來,并放入切片中。
- 第 17 行,對 sceneList 字符串切片進行排序。排序時,sceneList 會被修改。
- 第 20 行,輸出排好序的 map 的鍵。
sort.Strings 的作用是對傳入的字符串切片進行字符串字符的升序排列。排序接口的使用將在后面的章節(jié)中介紹。
map元素的刪除和清空
使用 delete() 函數(shù)從 map 中刪除鍵值對
使用 delete() 內(nèi)建函數(shù)從 map 中刪除一組鍵值對,delete() 函數(shù)的格式如下:
delete(map, 鍵)
- map 為要刪除的 map 實例。
- 鍵為要刪除的 map 鍵值對中的鍵。
從 map 中刪除一組鍵值對可以通過下面的代碼來完成
代碼輸出如下:
route 66
china 960
這個例子中使用 delete() 函數(shù)將 brazil 從 scene 這個 map 中刪除了。
清空 map 中的所有元素
有意思的是,Go 語言中并沒有為 map 提供任何清空所有元素的函數(shù)、方法。清空 map 的唯一辦法就是重新 make 一個新的 map。不用擔(dān)心垃圾回收的效率,Go 語言中的并行垃圾回收效率比寫一個清空函數(shù)高效多了。
Go語言sync.Map
Go 語言中的 map 在并發(fā)情況下,只讀是線程安全的,同時讀寫線程不安全。
下面來看下并發(fā)情況下讀寫 map 時會出現(xiàn)的問題,代碼如下
運行代碼會報錯,輸出如下:
fatal error: concurrent map read and map write
運行時輸出提示:并發(fā)的 map 讀寫。也就是說使用了兩個并發(fā)函數(shù)不斷地對 map 進行讀和寫而發(fā)生了競態(tài)問題。map 內(nèi)部會對這種并發(fā)操作進行檢查并提前發(fā)現(xiàn)。
需要并發(fā)讀寫時,一般的做法是加鎖,但這樣性能并不高。Go 語言在 1.9 版本中提供了一種效率較高的并發(fā)安全的 sync.Map。sync.Map 和 map 不同,不是以語言原生形態(tài)提供,而是在 sync 包下的特殊結(jié)構(gòu)。
sync.Map有以下特性:
- 無須初始化,直接聲明即可。
- sync.Map 不能使用 map 的方式進行取值和設(shè)置等操作,而是使用 sync.Map 的方法進行調(diào)用。Store 表示存儲,Load 表示獲取,Delete 表示刪除。
- 使用 Range 配合一個回調(diào)函數(shù)進行遍歷操作,通過回調(diào)函數(shù)返回內(nèi)部遍歷出來的值。Range 參數(shù)中的回調(diào)函數(shù)的返回值功能是:需要繼續(xù)迭代遍歷時,返回 true;終止迭代遍歷時,返回 false。
并發(fā)安全的 sync.Map?演示代碼如下
代碼輸出如下:
100 true
iterate: egypt 200
iterate: greece 97
代碼說明如下:
- 第 10 行,聲明 scene,類型為 sync.Map。注意,sync.Map 不能使用 make 創(chuàng)建。
- 第 13~15 行,將一系列鍵值對保存到 sync.Map 中,sync.Map 將鍵和值以 interface{} 類型進行保存。
- 第 18 行,提供一個 sync.Map 的鍵給 scene.Load() 方法后將查詢到鍵對應(yīng)的值返回。
- 第 21 行,sync.Map 的 Delete 可以使用指定的鍵將對應(yīng)的鍵值對刪除。
- 第 24 行,Range() 方法可以遍歷 sync.Map,遍歷需要提供一個匿名函數(shù),參數(shù)為 k、v,類型為 interface{},每次 Range() 在遍歷一個元素時,都會調(diào)用這個匿名函數(shù)把結(jié)果返回。
sync.Map 沒有提供獲取 map 數(shù)量的方法,替代方法是獲取時遍歷自行計算數(shù)量。sync.Map 為了保證并發(fā)安全有一些性能損失,因此在非并發(fā)情況下,使用 map 相比使用 sync.Map 會有更好的性能。
Go語言list(列表)
列表是一種非連續(xù)存儲的容器,由多個節(jié)點組成,節(jié)點通過一些變量記錄彼此之間的關(guān)系。列表有多種實現(xiàn)方法,如單鏈表、雙鏈表等。
列表的原理可以這樣理解:假設(shè) A、B、C 三個人都有電話號碼,如果 A 把號碼告訴給 B,B 把號碼告訴給 C,這個過程就建立了一個單鏈表結(jié)構(gòu),如下圖所示。
圖:三人單向通知電話號碼形成單鏈表結(jié)構(gòu)
如果在這個基礎(chǔ)上,再從 C 開始將自己的號碼給自己知道號碼的人,這樣就形成了雙鏈表結(jié)構(gòu),如下圖所示。
圖:三人相互通知電話號碼形成雙鏈表結(jié)構(gòu)
那么如果需要獲得所有人的號碼,只需要從 A 或者 C 開始,要求他們將自己的號碼發(fā)出來,然后再通知下一個人如此循環(huán)。這個過程就是列表遍歷。
如果 B 換號碼了,他需要通知 A 和 C,將自己的號碼移除。這個過程就是列表元素的刪除操作,如下圖所示。
圖:從雙鏈表中刪除一人的電話號碼
在 Go 語言中,將列表使用 container/list 包來實現(xiàn),內(nèi)部的實現(xiàn)原理是雙鏈表。列表能夠高效地進行任意位置的元素插入和刪除操作。
初始化列表
list 的初始化有兩種方法:New 和聲明。兩種方法的初始化效果都是一致的。
1) 通過 container/list 包的 New 方法初始化 list
變量名 := list.New()
2) 通過聲明初始化list
var 變量名 list.List
列表與切片和 map 不同的是,列表并沒有具體元素類型的限制。因此,列表的元素可以是任意類型。這既帶來遍歷,也會引來一些問題。給一個列表放入了非期望類型的值,在取出值后,將 interface{} 轉(zhuǎn)換為期望類型時將會發(fā)生宕機。
在列表中插入元素
雙鏈表支持從隊列前方或后方插入元素,分別對應(yīng)的方法是 PushFront 和 PushBack。
提示
這兩個方法都會返回一個 *list.Element 結(jié)構(gòu)。如果在以后的使用中需要刪除插入的元素,則只能通過 *list.Element 配合 Remove() 方法進行刪除,這種方法可以讓刪除更加效率化,也是雙鏈表特性之一。
下面代碼展示如何給list添加元素
代碼說明如下:
- 第 1 行,創(chuàng)建一個列表實例。
- 第 3 行,將 fist 字符串插入到列表的尾部,此時列表是空的,插入后只有一個元素。
- 第 4 行,將數(shù)值 67 放入列表。此時,列表中已經(jīng)存在 fist 元素,67 這個元素將被放在 fist 的前面。
列表插入元素的方法如下表所示。
| InsertAfter(v interface {}, mark * Element) * Element | 在 mark 點之后插入元素,mark 點由其他插入函數(shù)提供 |
| InsertBefore(v interface?{}, mark * Element) *Element | 在 mark 點之前插入元素,mark 點由其他插入函數(shù)提供 |
| PushBackList(other *List) | 添加 other 列表元素到尾部 |
| PushFrontList(other *List) | 添加 other 列表元素到頭部 |
從列表中刪除元素
列表的插入函數(shù)的返回值會提供一個 *list.Element 結(jié)構(gòu),這個結(jié)構(gòu)記錄著列表元素的值及和其他節(jié)點之間的關(guān)系等信息。從列表中刪除元素時,需要用到這個結(jié)構(gòu)進行快速刪除。
列表操作元素
代碼說明如下:
第 6 行,創(chuàng)建列表實例。
第 9 行,將 canon 字符串插入到列表的尾部。
第 12 行,將 67 數(shù)值添加到列表的頭部。
第 15 行,將 fist 字符串插入到列表的尾部,并將這個元素的內(nèi)部結(jié)構(gòu)保存到 element 變量中。
第 18 行,使用 element 變量,在 element 的位置后面插入 high 字符串。
第 21 行,使用 element 變量,在 element 的位置前面插入 noon 字符串。
第 24 行,移除 element 變量對應(yīng)的元素。
下表中展示了每次操作后列表的實際元素情況。
| 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() 函數(shù)獲取頭元素,遍歷時只要元素不為空就可以繼續(xù)進行。每一次遍歷調(diào)用元素的 Next,如代碼中第 9 行所示
l := list.New() // 尾部添加 l.PushBack("canon") // 頭部添加 l.PushFront(67) for i := l.Front(); i != nil; i = i.Next() { fmt.Println(i.Value) } 代碼輸出如下:
67
canon
代碼說明如下:
- 第 1 行,創(chuàng)建一個列表實例。
- 第 4 行,將 canon 放入列表尾部。
- 第 7 行,在隊列頭部放入 67。
- 第 9 行,使用 for 語句進行遍歷,其中 i:=l.Front() 表示初始賦值,只會在一開始執(zhí)行一次;每次循環(huán)會進行一次 i!=nil 語句判斷,如果返回 false,表示退出循環(huán),反之則會執(zhí)行 i=i.Next()。
- 第 10 行,使用遍歷返回的 *list.Element 的 Value 成員取得放入列表時的原值。
轉(zhuǎn)載于:https://www.cnblogs.com/596014054-yangdongsheng/p/10231412.html
總結(jié)
以上是生活随笔為你收集整理的Go第三篇之大话容器的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 数码管显示 0-9999计数器
- 下一篇: 【强化学习实战】基于gym和tensor