Golang高效實踐之array、slice、map實踐

  • 2019 年 10 月 3 日
  • 筆記

前言

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("%Tn", 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:%Tns type:%Tn", 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