標籤:alice 設定 run mutex 一個 忽略 pre 解析 也會
轉自:https://se77en.cc/Array(數組)內部機制
在 Go 語言中數組是固定長度的資料類型,它包含相同類型的連續的元素,這些元素可以是內建類型,像數字和字串,也可以是結構類型,元素可以通過唯一的索引值訪問,從 0 開始。
數組是很有價值的資料結構,因為它的記憶體配置是連續的,記憶體連續意味著可是讓它在 CPU 緩衝中待更久,所以迭代數組和移動元素都會非常迅速。
數組聲明和初始化
通過指定資料類型和元素個數(數組長度)來聲明數組。
// 聲明一個長度為5的整數數組var array [5]int
一旦數組被聲明了,那麼它的資料類型跟長度都不能再被改變。如果你需要更多的元素,那麼只能建立一個你想要長度的新的數組,然後把原有數組的元素拷貝過去。
Go 語言中任何變數被聲明時,都會被預設初始化為各自類型對應的 0 值,數組當然也不例外。當一個數組被聲明時,它裡麵包含的每個元素都會被初始化為 0 值。
一種快速建立和初始化數組的方法是使用數組字面值。數組字面值允許我們聲明我們需要的元素個數並指定資料類型:
// 聲明一個長度為5的整數數組// 初始化每個元素array := [5]int{7, 77, 777, 7777, 77777}
如果你把長度寫成 ...
,Go 編譯器將會根據你的元素來推匯出長度:
// 通過初始化值的個數來推匯出數組容量array := [...]int{7, 77, 777, 7777, 77777}
如果我們知道想要數組的長度,但是希望對指定位置元素初始化,可以這樣:
// 聲明一個長度為5的整數數組// 為索引為1和2的位置指定元素初始化// 剩餘元素為0值array := [5]int{1: 77, 2: 777}
使用數組
使用 []
操作符來訪問數組元素:
array := [5]int{7, 77, 777, 7777, 77777}// 改變索引為2的元素的值array[2] = 1
我們可以定義一個指標數組:
array := [5]*int{0: new(int), 1: new(int)}// 為索引為0和1的元素賦值*array[0] = 7*array[1] = 77
在 Go 語言中數組是一個值,所以可以用它來進行賦值操作。一個數組可以被賦值給任意相同類型的數組:
var array1 [5]stringarray2 := [5]string{"Red", "Blue", "Green", "Yellow", "Pink"}array1 = array2
注意數組的類型同時包括數組的長度和可以被儲存的元素類型,數群組類型完全相同才可以互相賦值,比如下面這樣就不可以:
var array1 [4]stringarray2 := [5]string{"Red", "Blue", "Green", "Yellow", "Pink"}array1 = array2// 編譯器會報錯Compiler Error:cannot use array2 (type [5]string) as type [4]string in assignment
拷貝一個指標數組實際上是拷貝指標值,而不是指標指向的值:
var array1 [3]*stringarray2 := [3]*string{new(string), new(string), new(string)}*array2[0] = "Red"*array2[1] = "Blue"*array2[2] = "Green"array1 = array2// 賦值完成後,兩組指標數組指向同一字串
多維陣列
數組總是一維的,但是可以組合成多維的。多維陣列通常用於有父子關係的資料或者是座標係數據:
// 聲明一個二維數組var array [4][2]int// 使用數組字面值聲明並初始化array := [4][2]int{{10, 11}, {20, 21}, {30, 31}, {40, 41}}// 指定外部數組索引位置初始化array := [4][2]int{1: {20, 21}, 3: {40, 41}}// 同時指定內外部數組索引位置初始化array := [4][2]int{1: {0: 20}, 3: {1: 41}}
同樣通過 []
操作符來訪問數組元素:
var array [2][2]intarray[0][0] = 0array[0][1] = 1array[1][0] = 2array[1][1] = 3
也同樣的相同類型的多維陣列可以相互賦值:
var array1 = [2][2]intvar array2 = [2][2]intarray[0][0] = 0array[0][1] = 1array[1][0] = 2array[1][1] = 3array1 = array2
因為數組是值,我們可以拷貝單獨的維:
var array3 [2]int = array1[1]var value int = array1[1][0]
在函數中傳遞數組
在函數中傳遞數組是非常昂貴的行為,因為在函數之間傳遞變數永遠是傳遞值,所以如果變數是數組,那麼意味著傳遞整個數組,即使它很大很大很大。。。
舉個栗子,建立一個有百萬元素的整形數組,在64位的機器上它需要8兆的記憶體空間,來看看我們聲明它和傳遞它時發生了什麼:
var array [1e6]intfoo(array)func foo(array [1e6]int) { ...}
每一次 foo
被調用,8兆記憶體將會被分配在棧上。一旦函數返回,會彈棧並釋放記憶體,每次都需要8兆空間。
Go 語言當然不會這麼傻,有更好的方法來在函數中傳遞數組,那就是傳遞指向數組的指標,這樣每次只需要分配8位元組記憶體:
var array [1e6]intfoo(&array)func foo(array *[1e6]int){ ...}
但是注意如果你在函數中改變指標指向的值,那麼原始數組的值也會被改變。幸運的是 slice
(切片)可以幫我們處理好這些問題,來一起看看。
Slice(切片)內部機制和基礎
slice 是一種可以動態數組,可以按我們的希望增長和收縮。它的增長操作很容易使用,因為有內建的 append
方法。我們也可以通過 relice 操作化簡 slice。因為 slice 的底層記憶體是連續分配的,所以 slice 的索引,迭代和記憶體回收效能都很好。
slice 是對底層數組的抽象和控制。它包含 Go 需要對底層數組管理的三種中繼資料,分別是:
- 指向底層數組的指標
- slice 中元素的長度
- slice 的容量(可供增長的最大值)
建立和初始化
Go 中建立 slice 有很多種方法,我們一個一個來看。
第一個方法是使用內建的函數 make
。當我們使用 make
建立時,一個選項是可以指定 slice 的長度:
slice := make([]string, 5)
如果只指定了長度,那麼容量預設等於長度。我們可以分別指定長度和容量:
slice := make([]int, 3, 5)
當我們分別指定了長度和容量,我們建立的 slice 就可以擁有一開始並沒有訪問的底層數組的容量。上面代碼的 slice 中,可以訪問3個元素,但是底層數組有5個元素。兩個與長度不相干的元素可以被 slice 來用。新建立的 slice 同樣可以共用底層數組和已存在的容量。
不允許建立長度大於容量的 slice:
slice := make([]int, 5, 3)Compiler Error:len larger than cap in make([]int)
慣用的建立 slice 的方法是使用 slice 字面量。跟建立數組很類似,不過不用指定 []
裡的值。初始的長度和容量依賴於元素的個數:
// 建立一個字串 slice// 長度和容量都是 5slice := []string{"Red", "Blue", "Green", "Yellow", "Pink"}
在使用 slice 字面量建立 slice 時有一種方法可以初始化長度和容量,那就是初始化索引。下面是個例子:
// 建立一個字串 slice// 初始化一個有100個元素的空的字串 sliceslice := []string{99: ""}
nil 和 empty slice
有的時候我們需要建立一個 nil slice,建立一個 nil slice 的方法是聲明它但不初始化它:
var slice []int
建立一個 nil slice 是建立 slice 最基本的方法,很多標準庫和內建函數都可以使用它。當我們想要表示一個並不存在的 slice 時它變得非常有用,比如一個返回 slice 的函數中發生異常的時候。
建立 empty slice 的方法就是聲明並初始化一下:
// 使用 make 建立silce := make([]int, 0)// 使用 slice 字面值建立slice := []int{}
empty slice 包含0個元素並且底層數組沒有分配儲存空間。當我們想要表示一個空集合時它很有用處,比如一個資料庫查詢返回0個結果。
不管我們用 nil slice 還是 empty slice,內建函數 append
,len
和cap
的工作方式完全相同。
使用 slice
為一個指定索引值的 slice 賦值跟之前數組賦值的做法完全相同。改變單個元素的值使用 []
操作符:
slice := []int{10, 20, 30, 40, 50}slice[1] = 25
我們可以在底層數組上對一部分資料進行 slice 操作,來建立一個新的 slice:
// 長度為5,容量為5slice := []int{10, 20, 30, 40, 50}// 長度為2,容量為4newSlice := slice[1:3]
在 slice 操作之後我們得到了兩個 slice,它們共用底層數組。但是它們能訪問底層數組的範圍卻不同,newSlice 不能訪問它頭指標前面的值。
計算任意 new slice 的長度和容量可以使用下面的公式:
對於 slice[i:j] 和底層容量為 k 的數組長度:j - i容量:k - i
必須再次明確一下現在是兩個 slice 共用底層數組,因此只要有一個 slice 改變了底層數組的值,那麼另一個也會隨之改變:
slice := []int{10, 20, 30, 40, 50}newSlice := slice[1:3]newSlice[1] = 35
改變 newSlice 的第二個元素的值,也會同樣改變 slice 的第三個元素的值。
一個 slice 只能訪問它長度範圍內的索引,試圖訪問超出長度範圍的索引會產生一個執行階段錯誤。容量只可以用來增長,它只有被合并到長度才可以被訪問(這句話有誤,解釋見藍色字型):
slice := []int{10, 20, 30, 40, 50}newSlice := slice[1:3]newSlice[3] = 45Runtime Exception:panic: runtime error: index out of range
容量可以被合并到長度裡,通過內建的 append
函數。也就是說容量是不可以改變的,長度是可以通過append增長的
《Go並發編程實戰 第一版》P55 有個很好的解釋:
“我們可以把切片想象成朝向底層數組的一個視窗。這個視窗是我們查看底層數組中元素值的途徑。這個值的長度就是我們當前可以看到的低成數組中的元素值的數量。而他的容量則表示我們最多看到多少個當前底層數組中的元素值”
array := [...]string{"Go","Python"."Java","C","C++","PHP"}slice := array[:4]slice[4] //會引發panic,索引值超過切片的長度。可以看到Go到Cslice = slice[:cap(slice)] //擴大了slice的長度,現在slice的長度和容留一樣大.可以看到Go到PHPslice = slice[:4:cap(slice)-1] //slice最多看到Go到C++
slice 增長
slice 比 數組的優勢就在於它可以按照我們的需要增長,我們只需要使用 append
方法,然後 Go 會為我們做好一切。
使用 append
方法時我們需要一個源 slice 和需要附加到它裡面的值。當 append
方法返回時,它返回一個新的 slice,append
方法總是增長 slice 的長度,另一方面,如果源 slice 的容量足夠,那麼底層數組不會發生改變,否則會重新分配記憶體空間。
// 建立一個長度和容量都為5的 sliceslice := []int{10, 20, 30, 40, 50}// 建立一個新的 slicenewSlice := slice[1:3]// 為新的 slice append 一個值newSlice = append(newSlice, 60)
因為 newSlice 有可用的容量,所以在 append
操作之後 slice 索引為 3 的值也變成了 60,之前說過這是因為 slice 和 newSlice 共用同樣的底層數組。
如果沒有足夠可用的容量,append
函數會建立一個新的底層數組,拷貝已存在的值和將要被附加的新值:
// 建立長度和容量都為4的 sliceslice := []int{10, 20, 30, 40}// 附加一個新值到 slice,因為超出了容量,所以會建立新的底層數組newSlice := append(slice, 50)
append
函數重新建立底層數組時,容量會是現有元素的兩倍(前提是元素個數小於1000),如果元素個數超過1000,那麼容量會以 1.25 倍來增長。
slice 的第三個索引參數
slice 還可以有第三個索引參數來限定容量,它的目的不是為了增加容量,而是提供了對底層數組的一個保護機制,以方便我們更好的控制 append
操作,舉個栗子:
source := []string{"apple", "orange", "plum", "banana", "grape"}// 接著我們在源 slice 之上建立一個新的 sliceslice := source[2:3:4]
新建立的 slice 長度為 1,容量為 2,可以看出長度和容量的計算公式也很簡單:
對於 slice[i:j:k] 或者 [2:3:4]長度: j - i 或者 3 - 2容量: k - i 或者 4 - 2
如果我們試圖設定比可用容量更大的容量,會得到一個執行階段錯誤:
slice := source[2:3:6]Runtime Error:panic: runtime error: slice bounds out of range
限定容量最大的用處是我們在建立新的 slice 時候限定容量與長度相同,這樣以後再給新的 slice 增加元素時就會分配新的底層數組,而不會影響原有 slice 的值:
source := []string{"apple", "orange", "plum", "banana", "grape"}// 接著我們在源 slice 之上建立一個新的 slice// 並且設定長度和容量相同slice := source[2:3:3]// 添加一個新元素slice = append(slice, "kiwi")
如果沒有第三個索引參數限定,添加 kiwi 這個元素時就會覆蓋掉 banana。
內建函數 append
是一個變參函數,意思就是你可以一次添加多個元素,比如:
s1 := []int{1, 2}s2 := []int{3, 4}fmt.Printf("%v\n", append(s1, s2...))Output:[1 2 3 4]
迭代 slice
slice 也是一種集合,所以可以被迭代,用 for
配合 range
來迭代:
slice := []int{10, 20, 30, 40, 50}for index, value := range slice { fmt.Printf("Index: %d Value: %d\n", index, value)}Output:Index: 0 Value: 10Index: 1 Value: 20Index: 2 Value: 30Index: 3 Value: 40Index: 4 Value: 50
當迭代時 range
關鍵字會返回兩個值,第一個是索引值,第二個是索引位置值的拷貝。注意:返回的是值的拷貝而不是引用,如果我們把值的地址作為指標使用,會得到一個錯誤,來看看為啥:
slice := []int{10, 20, 30 ,40}for index, value := range slice { fmt.Printf("Value: %d Value-Addr: %X ElemAddr: %X\n", value, &value, &slice[index])}Output:Value: 10 Value-Addr: 10500168 ElemAddr: 1052E100Value: 20 Value-Addr: 10500168 ElemAddr: 1052E104Value: 30 Value-Addr: 10500168 ElemAddr: 1052E108Value: 40 Value-Addr: 10500168 ElemAddr: 1052E10C
value 變數的地址總是相同的因為它只是包含一個拷貝。如果想得到每個元素的真是地址可以使用 &slice[index]。
如果不需要索引值,可以使用 _
操作符來忽略它:
slice := []int{10, 20, 30, 40}for _, value := range slice { fmt.Printf("Value: %d\n", value)}Output:Value: 10Value: 20Value: 30Value: 40
range
總是從開始一次遍曆,如果你想控制遍曆的step,就用傳統的 for
迴圈:
slice := []int{10, 20, 30, 40}for index := 2; index < len(slice); index++ { fmt.Printf("Index: %d Value: %d\n", index, slice[index])}Output:Index: 2 Value: 30Index: 3 Value: 40
同數組一樣,另外兩個內建函數 len
和 cap
分別返回 slice 的長度和容量。
多維 slice
也是同數組一樣,slice 可以組合為多維的 slice:
slice := [][]int{{10}, {20, 30}}
需要注意的是使用 append
方法時的行為,比如我們現在對 slice[0] 增加一個元素:
slice := [][]int{{10}, {20, 30}}slice[0] = append(slice[0], 20)
那麼只有 slice[0] 會重新建立底層數組,slice[1] 則不會。
在函數間傳遞 slice
在函數間傳遞 slice 是很廉價的,因為 slice 相當於是指向底層數組的指標,讓我們建立一個很大的 slice 然後傳遞給函數調用它:
slice := make([]int, 1e6)slice = foo(slice)func foo(slice []int) []int { ... return slice}
在 64 位元的機器上,slice 需要 24 位元組的記憶體,其中指標部分需要 8 位元組,長度和容量也分別需要 8 位元組。
Map內部機制
map 是一種無序的索引值對的集合。map 最重要的一點是通過 key 來快速檢索資料,key 類似於索引,指向資料的值。
map 是一種集合,所以我們可以像迭代數組和 slice 那樣迭代它。不過,map 是無序的,我們無法決定它的返回順序,這是因為 map 是使用 hash 表來實現的。
map 的 hash 表包含了一個桶集合(collection of buckets)。當我們儲存,移除或者尋找索引值對(key/value pair)時,都會從選擇一個桶開始。在映射(map)操作過程中,我們會把指定的索引值(key)傳遞給 hash 函數(又稱散列函數)。hash 函數的作用是產生索引,索引均勻的分布在所有可用的桶上。hash 表演算法詳見:July的部落格–從頭到尾徹底解析 hash 表演算法
建立和初始化
Go 語言中有多種方法建立和初始化 map。我們可以使用內建函數 make
也可以使用 map 字面值:
// 通過 make 來建立dict := make(map[string]int)// 通過字面值建立dict := map[string]string{"Red": "#da1337", "Orange": "#e95a22"}
使用字面值是建立 map 慣用的方法(為什麼不使用make)。初始化 map 的長度依賴於索引值對的數量。
map 的鍵可以是任意內建類型或者是 struct 類型,map 的值可以是使用 ==
操作符的運算式。slice,function 和 包含 slice 的 struct 類型不可以作為 map 的鍵,否則會編譯錯誤:
dict := map[[]string]int{}Compiler Exception:invalid map key type []string
使用 map
給 map 賦值就是指定合法類型的鍵,然後把值賦給鍵:
colors := map[string]string{}colors["Red"] = "#da1337"
如果不初始化 map,那麼就會建立一個 nil map。nil map 不能用來存放索引值對,否則會報執行階段錯誤:
var colors map[string]stringcolors["Red"] = "#da1337"Runtime Error:panic: runtime error: assignment to entry in nil map
測試 map 的鍵是否存在是 map 操作的重要部分,因為它可以讓我們判斷是否可以執行一個操作,或者是往 map 裡緩衝一個值。它也可以被用來比較兩個 map 的索引值對是否匹配或者缺失。
從 map 裡檢索一個值有兩種選擇,我們可以同時檢索值並且判斷鍵是否存在:
value, exists := colors["Blue"]if exists { fmt.Println(value)}
另一種選擇是只傳回值,然後判斷是否是零值來確定鍵是否存在。但是只有你確定零值是非法值的時候這招才管用:
value := colors["Blue"]if value != "" { fmt.Println(value)}
當索引一個 map 取值時它總是會返回一個值,即使鍵不存在。上面的例子就返回了對應類型的零值。
迭代一個 map 和迭代數組和 slice 是一樣的,使用 range
關鍵字,不過在迭代 map 時我們不使用 index/value 而使用 key/value 結構:
colors := map[string]string{ "AliceBlue": "#f0f8ff", "Coral": "#ff7F50", "DarkGray": "#a9a9a9", "ForestGreen": "#228b22",}for key, value := range colors { fmt.Printf("Key: %s Value: %s\n", key, value)}
如果我們想要從 map 中移除一個索引值對,使用內建函數 delete
(要是也能返回移除是否成功就好了,哎。。。):
delete(colors, "Coral")for key, value := range colors { fmt.Println("Key: %s Value: %s\n", key, value)}
在函數間傳遞 map
在函數間傳遞 map 不是傳遞 map 的拷貝。所以如果我們在函數中改變了 map,那麼所有引用 map 的地方都會改變:
func main() { colors := map[string]string{ "AliceBlue": "#f0f8ff", "Coral": "#ff7F50", "DarkGray": "#a9a9a9", "ForestGreen": "#228b22", } for key, value := range colors { fmt.Printf("Key: %s Value: %s\n", key, value) } removeColor(colors, "Coral") for key, value := range colors { fmt.Printf("Key: %s Value: %s\n", key, value) }}func removeColor(colors map[string]string, key string) { delete(colors, key)}
執行會得到以下結果:
Key: AliceBlue Value: #F0F8FFKey: Coral Value: #FF7F50Key: DarkGray Value: #A9A9A9Key: ForestGreen Value: #228B22 Key: AliceBlue Value: #F0F8FFKey: DarkGray Value: #A9A9A9Key: ForestGreen Value: #228B22
可以看出來傳遞 map 也是十分廉價的,類似 slice。
Set
Go 語言本身是不提供 set 的,但是我們可以自己實現它,下面就來試試:
package mainimport( "fmt" "sync")type Set struct { m map[int]bool sync.RWMutex}func New() *Set { return &Set{ m: map[int]bool{}, }}func (s *Set) Add(item int) { s.Lock() defer s.Unlock() s.m[item] = true}func (s *Set) Remove(item int) { s.Lock() s.Unlock() delete(s.m, item)}func (s *Set) Has(item int) bool { s.RLock() defer s.RUnlock() _, ok := s.m[item] return ok}func (s *Set) Len() int { return len(s.List())}func (s *Set) Clear() { s.Lock defer s.Unlock() s.m = map[int]bool{}}func (s *Set) IsEmpty() bool { if s.Len() == 0 { return true } return false}func (s *Set) List() []int { s.RLock() defer s.RUnlock() list := []int{} for item := range s.m { list = append(list, item) } return list}func main() { // 初始化 s := New() s.Add(1) s.Add(1) s.Add(2) s.Clear() if s.IsEmpty() { fmt.Println("0 item") } s.Add(1) s.Add(2) s.Add(3) if s.Has(2) { fmt.Println("2 does exist") } s.Remove(2) s.Remove(3) fmt.Println("list of all items", S.List())}
注意我們只是使用了 int 作為鍵,你可以自己實現用 interface{} 作為鍵,做成更通用的 Set,另外,這個實現是安全執行緒的。
總結
- 數組是 slice 和 map 的底層結構。
- slice 是 Go 裡面慣用的集合資料的方法,map 則是用來儲存索引值對。
- 內建函數
make
用來建立 slice 和 map,並且為它們指定長度和容量等等。slice 和 map 字面值也可以做同樣的事。
- slice 有容量的約束,不過可以通過內建函數
append
來增加元素。
- map 沒有容量一說,所以也沒有任何增長限制。
- 內建函數
len
可以用來獲得 slice 和 map 的長度。
- 內建函數
cap
只能作用在 slice 上。
- 可以通過組合方式來建立多維陣列和 slice。map 的值可以是 slice 或者另一個 map。slice 不能作為 map 的鍵。
- 在函數之間傳遞 slice 和 map 是相當廉價的,因為他們不會傳遞底層數組的拷貝。
Go 語言中的 Array,Slice,Map 和 Set