成對鍵值的 map


許多語言中都會有的成對鍵值資料結構,在 Go 中是以內建型態 map 來實作,格式為 map[keyType]valueType

建立與初始 map

想要建立例一個 map 實例,但尚無任何鍵值對,可以使用 make 函式,例如:

package main

import "fmt"

func main() {
    passwords := make(map[string]int)
    fmt.Println(passwords)      // map[]
    fmt.Println(len(passwords)) // 0

    passwords["caterpillar"] = 123456
    passwords["monica"] = 54321
    fmt.Println(passwords)                // map[caterpillar:123456 monica:54321]
    fmt.Println(len(passwords))           // 2
    fmt.Println(passwords["caterpillar"]) // 123456
    fmt.Println(passwords["monica"])      // 54321
}

在上例中,passwords 是個參考,指向 make(map[string]int) 建立的 map 實例。

類似一些語言(例如 Python),要設定一個鍵值對應時,使用 []= 指定,要取得鍵對應的值時,使用 [] 指定鍵,這會傳回對應的值,想知道 map 中的鍵數,可以使用 len 函式。

要注意的是,可用來做為鍵的值,必須是 comparable,就目前來說,你要先知道的 comparable 型態有布林、數字、字串、指標(pointer)、channel、interface、struct,或者含有這這些型態的陣列,這些是都可以使用 == 來比較的值;而 slice、map 與函式,就不能用來做為鍵。

如果已知 map 中會有的鍵值對,則可以如下建立 map:

package main

import "fmt"

func main() {
    passwords := map[string]int{
        "caterpillar": 123456,
        "monica":      54321,
    }

    fmt.Println(passwords)                // map[monica:54321 caterpillar:123456]
    fmt.Println(len(passwords))           // 2
    fmt.Println(passwords["caterpillar"]) // 12345
    fmt.Println(passwords["monica"])      // 54321
}

如果 passwords 建立時,最後一個鍵值項目後不換行,那麼最後一個逗號就不需要,例如:

passwords := map[string]int {"caterpillar" : 123456, "monica" : 54321}

實際上,你也可以寫 passwords := map[string]int {},來建立一個沒有任何鍵值對的 map,這相當於寫 passwords := make(map[string]int),不過,若是 var passwords map[string]int 的話,只是建立一個參考名稱 passwords,預設零值是 nil,也就是相當於 var passwords map[string]int = nil 的意思。

也就是說,var passwords map[string]int 宣告了一個參考型態,兩個 map 型態的名稱,可以指向同一個 map 實例,透過其中一個名稱來改變 map 內容,從另一個名稱就可以獲得對應的修改:

package main

import "fmt"

func main() {
    passwds1 := map[string]int{"caterpillar": 123456}
    passwds2 := passwds1

    fmt.Println(passwds1) // map[caterpillar:123456]

    passwds2["monica"] = 54321
    fmt.Println(passwds1) // map[monica:54321 caterpillar:123456]
}

鍵值存取、刪除

如方才所看到的,要設定一個鍵值對應時,使用 []= 指定,要取得鍵對應的值時,使用 [] 指定鍵,這會傳回對應的值,如果指定的鍵不存在,那麼會傳回值型態對應的零值,例如,若 passwords := map[string]int {"caterpillar" : 123456},那麼 passwords["monica"] 會傳回 0。

不過,更精確地說,使用 mapName[key] 時,會傳回兩個值(Go 中允許同時傳回多值),第一個是鍵對應的值,若沒有該鍵就傳回值型態的零值,第二個是布林值,指出鍵是否存在。例如:

package main

import "fmt"

func main() {
    passwds := map[string]int{"caterpillar": 123456}

    v, exists := passwds["monica"]
    fmt.Printf("%d %t\n", v, exists) // 0 false

    passwds["monica"] = 54321
    v, exists = passwds["monica"]
    fmt.Printf("%d %t\n", v, exists) // 54321 true
}

因此,若只是單純想測試鍵是否存在,只要用底線 _ 忽略傳回的值就可以了,例如:

package main

import "fmt"

func main() {
    passwds := map[string]int{"caterpillar": 123456}
    name := "caterpillar"
    _, exists := passwds[name]
    if exists {
        fmt.Printf("%s's password is %d\n", name, passwds[name])
    } else {
        fmt.Printf("No password for %s\n", name)
    }
}

exists 的指定與 if 的判斷也可以寫在同一行:

if _, exists := passwds[name]; exists {
    fmt.Printf("%s's password is %d\n", name, passwds[name])
} else {
    fmt.Printf("No password for %s\n", name)
}

如果想刪除某個鍵值,可以使用 delete 函式,例如 delete(passwds, "caterpillar")

迭代鍵值

如果要迭代 map 的鍵值,可以使用 for range,例如:

package main

import "fmt"

func main() {
    passwords := map[string]int{
        "caterpillar": 123456,
        "monica":      54321,
    }

    for name, passwd := range passwords {
        fmt.Printf("%s : %d\n", name, passwd)
    }
}

如果只是想迭代 map 的鍵,可以如下:

package main

import "fmt"

func main() {
    passwords := map[string]int{
        "caterpillar": 123456,
        "monica":      54321,
    }

    for name := range passwords {
        fmt.Printf("%s\n", name)
    }
}

如果只想迭代 map 的值,可以如下:

package main

import "fmt"

func main() {
    passwords := map[string]int{
        "caterpillar": 123456,
        "monica":      54321,
    }

    for _, passwd := range passwords {
        fmt.Printf("%d\n", passwd)
    }
}

如果想取得 map 中的鍵清單或者是值清單,方式之一是使用 slice 進行收集,例如:

package main

import "fmt"

func keys(m map[string]int) []string {
    ks := make([]string, 0, len(m))
    for k := range m {
        ks = append(ks, k)
    }
    return ks
}

func values(m map[string]int) []int {
    vs := make([]int, 0, len(m))
    for _, v := range m {
        vs = append(vs, v)
    }
    return vs
}

func main() {
    passwords := map[string]int{
        "caterpillar": 123456,
        "monica":      54321,
    }

    fmt.Println(keys(passwords))   // [caterpillar monica]
    fmt.Println(values(passwords)) // [123456 54321]
}

Go 的 map 在迭代時沒有一定的順序,如果想要有排序結果,必須自行處理,例如,針對鍵排序來進行迭代:

package main

import "sort"
import "fmt"

func main() {
    passwords := map[string]int{
        "caterpillar": 123456,
        "monica":      54321,
        "hamimi":      13579,
    }

    keys := make([]string, 0, len(passwords))
    for key := range passwords {
        keys = append(keys, key)
    }
    sort.Strings(keys)

    for _, key := range keys {
        fmt.Printf("%s : %d\n", key, passwords[key])
    }
}