はじめに
こちらを日本語訳してみます。
コンピュータサイエンスにおいて最も便利なデータ構造の一つがハッシュテーブルです。Goでは、このハッシュテーブルを実装した組み込みのマップ型を提供しています。マップは、高速な検索、追加、削除を実現します。
マップの宣言と初期化
Goでのマップの型はmap[KeyType]ValueType
と表されます。KeyType
は比較可能な任意の型であり、ValueType
は任意の型です。
var m map[string]int
マップ型は参照型であり、初期化されていないマップはnil
を指します。nil
マップは読み取りには空のマップとして振る舞いますが、書き込みを試みるとランタイムパニックを引き起こします。マップを初期化するにはmake
関数を使用します。
m = make(map[string]int)
マップの操作
Goはマップを扱うための馴染み深い構文を提供しています。この文はキー"route"
に値66を設定します:
m["route"] = 66
この文はキー"route"
の下に保存された値を取得し、新しい変数i
に割り当てます:
i := m["route"]
要求されたキーが存在しない場合、値型のゼロ値が得られます。この場合、値型はint
なので、ゼロ値は0
です:
j := m["root"]
// j == 0
組み込みのlen
関数はマップ内のアイテム数を返します:
n := len(m)
組み込みのdelete
関数はマップからエントリを削除します:
delete(m, "route")
delete
関数は何も返さず、指定されたキーが存在しない場合は何もしません。
2値の代入はキーの存在をテストします:
i, ok := m["route"]
この文では、最初の値(i
)にキー"route"
の下に保存された値が割り当てられます。そのキーが存在しない場合、i
は値型のゼロ値(0
)になります。2番目の値(ok
)は、キーがマップに存在する場合はtrue
、存在しない場合はfalse
のbool
です。
値を取得せずにキーのテストをするには、最初の値の代わりにアンダースコアを使用します:
_, ok := m["route"]
マップの内容を反復処理するには、range
キーワードを使用します:
for key, value := range m {
fmt.Println("Key:", key, "Value:", value)
}
データでマップを初期化するには、マップリテラルを使用します:
commits := map[string]int{
"rsc": 3711,
"r": 2138,
"gri": 1908,
"adg": 912,
}
空のマップを初期化するのに同じ構文を使用することができ、これはmake
関数を使用するのと機能的に同一です:
m = map[string]int{}
ゼロ値の活用
キーが存在しない場合にマップからゼロ値が得られることは便利です。
例えば、ブール値のマップをセットのようなデータ構造として使用できます(ブール型のゼロ値はfalse
であることを思い出してください)。この例では、ノードのリンクリストをトラバースし、その値を出力します。リスト内のサイクルを検出するためにノードポインタのマップを使用します。
type Node struct {
Next *Node
Value interface{}
}
var first *Node
visited := make(map[*Node]bool)
for n := first; n != nil; n = n.Next {
if visited[n] {
fmt.Println("cycle detected")
break
}
visited[n] = true
fmt.Println(n.Value)
}
visited[n]
の式は、n
が訪問済みであればtrue
、存在しなければfalse
となります。n
がマップ内に存在するかどうかをテストするために2値形式を使用する必要はありません。ゼロ値のデフォルトが私たちのためにそれを行います。
ゼロ値が役立つ別の例は、スライスのマップです。nil
スライスに追加することは新しいスライスを割り当てるので、スライスのマップに値を追加するのはワンライナーです。キーが存在するかどうかをチェックする必要はありません。次の例では、スライスpeople
がPerson
値で埋められます。各Person
にはName
とLikes
のスライスがあります。この例では、それぞれの好みに応じて人々のスライスを関連付けるマップを作成します。
type Person struct {
Name string
Likes []string
}
var people []*Person
likes := make(map[string][]*Person)
for _, p := range people {
for _, l := range p.Likes {
likes[l] = append(likes[l], p)
}
}
チーズが好きな人のリストを出力するには:
for _, p := range likes["cheese"] {
fmt.Println(p.Name, "likes cheese.")
}
ベーコンが好きな人の数を出力するには:
fmt.Println(len(likes["bacon"]), "people like bacon.")
range
とlen
はnil
スライスを長さ0のスライスとして扱うので、これらの最後の2つの例は、誰もチーズやベーコンが好きではない(それがどれほどありそうもないかにかかわらず)場合でも機能します。
キータイプ
前述の通り、マップのキーには比較可能な任意の型が使用できます。言語仕様はこれを正確に定義していますが、簡単に言うと、比較可能な型にはブール型、数値型、文字列型、ポインタ型、チャネル型、インターフェース型、およびこれらの型のみを含む構造体や配列があります。リストから明らかに欠けているのは、スライス、マップ、関数です。これらの型は==
を使用して比較することができず、マップのキーとして使用することはできません。
文字列、整数、その他の基本的な型がマップキーとして使用できるのは明らかですが、構造体キーが使用できることは予想外かもしれません。構造体は、複数の次元にわたってデータをキーとして使用することができます。例えば、このマップのマップは、国別のWebページのヒット数を集計するために使用できます:
hits := make(map[string]map[string]int)
これは文字列から(文字列からint
へのマップの)マップです。外側のマップの各キーはWebページへのパスで、それぞれの内側のマップがあります。内側のマップの各キーは2文字の国コードです。この式は、オーストラリアのユーザーがドキュメンテーションページを読み込んだ回数を取得します:
n := hits["/doc/"]["au"]
残念ながら、このアプローチではデータを追加する際に扱いにくくなります。与えられた外側のキーごとに内側のマップが存在するかどうかをチェックし、必要であれば作成しなければなりません:
func add(m map[string]map[string]int, path, country string) {
mm, ok := m[path]
if !ok {
mm = make(map[string]int)
m[path] = mm
}
mm[country]++
}
add(hits, "/doc/", "au")
一方、単一のマップを構造体キーとして使用する設計は、そのすべての複雑さを解消します:
type Key struct {
Path, Country string
}
hits := make(map[Key]int)
ベトナム人がホームページを訪問したときに、適切なカウンターを増やす(および場合によっては作成する)ことはワンライナーです:
hits[Key{"/", "vn"}]++
そして、スイス人が仕様書を読んだ回数を確認するのも同様に簡単です:
n := hits[Key{"/ref/spec", "ch"}]
並行性
マップは並行利用に対して安全ではありません。同時に読み書きを行った場合に何が起こるかは定義されていません。並行して実行されるゴルーチンからマップに読み書きする必要がある場合、アクセスは何らかの同期メカニズムによって仲介されなければなりません。マップを保護する一般的な方法の一つは、sync.RWMutexを使用することです。
この文は、マップと埋め込まれたsync.RWMutex
を含む匿名構造体を持つカウンタ変数を宣言します。
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()
マップに対する同時アクセスを安全に管理するためには、このような同期が必要になります。これにより、並行プログラミングにおけるデータの整合性を保ちながら、マップへのアクセスを効率的に行うことができます。
反復処理の順序
マップをrange
ループで反復処理するとき、反復処理の順序は指定されておらず、一回の反復から次へと同じであることが保証されていません。安定した反復処理の順序が必要な場合、その順序を指定する別のデータ構造を維持する必要があります。この例では、キーの順序でmap[int]string
を出力するために、キーのソートされたスライスを別途使用しています。
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])
}
この方法では、まずマップからすべてのキーを抽出し、それらをスライスに追加します。その後、sort.Ints
関数を使用してキーをソートし、ソートされた順序でマップの各エントリを出力します。このアプローチにより、マップのエントリをキーの順序で安定して反復処理することができます。