前言
Golang的slice類型為連續同類型數據提供了一個方便並且高效的實現方式。slice的實現是基於array,slice和map一樣是類似於指針語義,傳遞slice和map並不涉及底層數據結構的拷貝,相當於傳遞底層數據結構的指針。
Arrays數組
數組類型的定義需要指定長度和元素的類型。例如,[4]int表示一個四個整數的數組。數組的大小是固定的,數組的大小是類型的一部分,也就是說[4]int 和 [5]int是不同的類型,不能比較。數組可以按序號索引訪問,從0開始,s[n]表示訪問第n個元素。
var a [4]int a[0] = 1 i := a[0] // i == 1
數組不需要明確的初始化,默認是數組元素類型的零值:
// a[2] == 0
[4]int的內存分布為:
Go的數組是值語義,即數組變量代表整個數組,並不是一個指向數組第一個元素的指針(C語言)。這意味着當賦值或者傳遞數組時將會產生數組內容拷貝。
數組指定元素初始化:
b := [2]string{"Penn", "Teller"}
或者:
b := [...]string{"Penn", "Teller"}
上面兩種寫法b的類型都是[2]string
package main
import "fmt"
func main() { b := [...]string{"Penn", "Teller"} fmt.Printf("%T\n", b) }
程序輸出:
[2]string
Slices切片
由於數組的大小是固定的,不是很靈活,所以在Go的代碼里面不會經常出現。但是,slice是隨處可見的。slice是數組的基礎進行了封裝,更加強大和方便。
切片的定義為:[]T,其中T是切片元素的類型。不像數組,切片類型不用指定長度。例如:
letters := []string{"a", "b", "c", "d"}
letters的類型為[]string,而不是[4]string
切片可以用內建函數make創建,make的簽名為:
func make([]T, len, cap) []T
其中cap可選
var s []byte s = make([]byte, 5, 5) // s == []byte{0, 0, 0, 0, 0}
不指定cap:
s := make([]byte, 5)
切片的零值是nil。對零值調用len和cap函數將會返回0.
切片可以從一個已經存在的切片或者數組中切分生成,切分遵循的是半開閉原則,例如b[1:4]表達式創建一個切片包含b的序號1到3的元素,由此得到新的切片序號將會是從0到2。
b := []byte{'g', 'o', 'l', 'a', 'n', 'g'} // b[1:4] == []byte{'o', 'l', 'a'}, sharing the same storage as b
用於表示切分的開始和結束的序號都是可選的,默認分別是0和切片的長度。例如:
// b[:2] == []byte{'g', 'o'} // b[2:] == []byte{'l', 'a', 'n', 'g'} // b[:] == b
從數組中切分為切片:
package main import "fmt" func main() { x := [3]string{"我","是","Gopher"} s := x[:] fmt.Println(s) fmt.Printf("x type:%T\ns type:%T\n", x, s) }
程序輸出:
[我 是 Gopher] x type:[3]string s type:[]string
切片內部實現:
切片是一個數組段的描述,由一個指向數組的指針,數據段的長度(length),和它的最大能容納數據的大小(capacity):
上面提到的s,一開始的時候由make([]byte, 5)創建時,結構如下:
s=s[2:4]相關的結構體:
切分不會拷貝切片的數組,將會創建一個新的切片值指向原始的數組。這使得切片的操作像數組索引訪問一樣高效。因此,更改重新切分的切片元素也會影響到原始的切片:
d := []byte{'r', 'o', 'a', 'd'} e := d[2:] // e == []byte{'a', 'd'} e[1] = 'm' // e == []byte{'a', 'm'} // d == []byte{'r', 'o', 'a', 'm'}
如果是想讓重新切分的切片擁有獨立的內存數據,可以使用copy函數:
func copy(dst, src []T) int
例如:
t := make([]byte, len(s), (cap(s)+1)*2) copy(t, s) s = t
Map哈希表/字典
計算機科學中哈希表是一個很重要的數據結構,Go提供了內建的map類型用於實現哈希表。
Go map類型長這樣:
map[KeyType]ValueType
其中KeyType需要是可比較類型,可比較類型:
可比較類型是值可以用==和!=操作比較的類型,有:
Boolean 值是可以比較的。兩個boolean值如果都是true或者都是false,那么它們就是相等的。
Interger,Float 值是可以比較的。
Complex 值是可以比較的。如果real(u) == real(v) 並且 imag(u) == imag(v),那么兩個complex值相等。
String 值是可以比較的。
Pointer值是可以比較的。如果兩個指針值指向同一個變量那么這兩個指針值相等,或者都是nil。
Channel 值是可以比較的。
Interface值是可以比較的。如果兩個interface值得concrete type和value都相等(前提是concrete type是可比較的),那么這兩個interface相等。如果兩個interface都是nil那么也相等。
var a1 int = 3 var a2 int = 3 var ia1 interface{} var ia2 interface{} ia1 = a1 ia2 = a2 if ia1 == ia2 { fmt.Println("equal") }
程序輸出:
equal
Struct值是可比較的,前提是每個字段都是可比較的。如果兩個struct的每個字段都相等,那么這兩個struct相等。
type ST struct { name string age int } s1 := ST{"tom", 19} s2 := ST{"tom", 19} fmt.Println(s1 == s2)
程序輸出:
true
數組值是可以比較的,前提是數組元素類型是可以比較的。當兩個數據的每個元素都對應相等,那么這兩個數組是相等的。
a1 := [2]string{"iam", "handsome"} a2 := [2]string{"iam", "handsome"} fmt.Println(a1 == a2)
程序輸出:
true
需要特別說明的是如果兩個interface指向的實際類型(concrete type)是不可比較類型,如果比較這兩個interface將會觸發運行時panic,例如:
a1 := []int{1,3} a2 := []int{1,3} var ia1 interface{} var ia2 interface{} ia1 = a1 ia2 = a2 if ia1 == ia2 { fmt.Println("equal") }
程序運行結果:
panic: runtime error: comparing uncomparable type []int goroutine 1 [running]: main.main() /Users/haiweilu/saas/src/awesomeProject/channel/main.go:24 +0xb2
Slice,map和function 值是不能比較的。但是有一個特例,就是可以和nil比較,判斷slice,map和function是否是nil。
所以slice,map和funciton值不能作為map的key。map的ValueType可以是任意類型,當然也包括map類型。例如:
var m map[string]int
Map類型是引用類型,類似於指針和切片,所以上述m的值是nil,它指向一個還沒初始化的map,即map的零值是nil。對nil map值進行讀寫訪問會觸發運行時panic。為了避免這種情況,可以用內建函數make創建map:
m = make(map[string]int)
Make函數分配並初始化一個哈希表數據結構,並且返回一個指向這個結構的map值。
Map的使用
設置一個key為”route”,value為66的元素:
m["route"] = 66
根據key索引訪問value:
i := m["route"]
如果key對應的value不存在,將會返回該value類型對應的零值,例如:
j := m["root”],j的值是0
求map的元素數量:
n := len(m)
根據key刪除map對應的k-v:
delete(m, "route")
可以用”common,ok”表達式判斷map的key是否存在:
_, ok := m["route"]
如果”route”存在,ok為true,否則為false
遍歷map:
for key, value := range m { fmt.Println("Key:", key, "Value:", value) }
初始化map的另外一種方法:
commits := map[string]int{ "rsc": 3711, "r": 2138, "gri": 1908, "adg": 912, } m = map[string]int{}
用map實現set
由於map索引對應key不存在時返回value類型的零值,所以我們可以用map[KeyType]bool來實現一個set
struct作為map的key實現多維索引
例如:
type Key struct { Path, Country string } hits := make(map[Key]int) hits[Key{"/", "vn"}]++ 也可以這樣: n := hits[Key{"/ref/spec", "ch"}]
Map的並發
Map的操作不是原子操作,所以多個goroutine並發讀寫map會導致運行時panic。同時讀沒有問題。可以通過讀寫鎖的方式實現同步並發讀寫:
var counter = struct{ sync.RWMutex m map[string]int }{m: make(map[string]int)} 讀: counter.RLock() n := counter.m["some_key"] counter.RUnlock() fmt.Println("some_key:", n) 寫: counter.Lock() counter.m["some_key"]++ counter.Unlock()
有序map
Map中的key不保證順序,也就說保證每次遍歷同一個map的key返回順序都是一致的,如果需要key是有序的可以通過增加一個輔助的切片來實現:
import "sort" var m map[int]string var keys []int for k := range m { keys = append(keys, k) } sort.Ints(keys) for _, k := range keys { fmt.Println("Key:", k, "Value:", m[k]) }
總結
文檔介紹了array、slice和map的各種使用場景,希望能夠幫助大家少點踩坑。
參考
https://blog.golang.org/go-slices-usage-and-internals
https://blog.golang.org/go-maps-in-action
https://golang.org/ref/spec#Comparison_operators
我的博客即將同步至騰訊雲+社區,邀請大家一同入駐:https://cloud.tencent.com/developer/support-plan?invite_code=thg523juerih