The Go Blog

Goマップの活用

Andrew Gerrand
2013年2月6日

はじめに

コンピュータサイエンスで最も有用なデータ構造の1つはハッシュテーブルです。ハッシュテーブルの実装はさまざまな特性を持つものが存在しますが、一般的に高速な検索、追加、削除を提供します。Goはハッシュテーブルを実装する組み込みのマップ型を提供します。

宣言と初期化

Goのマップ型は次のようになります

map[KeyType]ValueType

ここで、KeyType比較可能な任意の型(これについては後述します)、ValueTypeは別のマップを含む任意の型になります。

この変数mは、文字列キーから整数値へのマップです

var m map[string]int

マップ型はポインタやスライスと同様に参照型であり、上記のmの値はnilです。初期化されたマップを指していません。nilマップは読み取り時には空のマップのように動作しますが、nilマップへの書き込みを試みるとランタイムパニックが発生します。そうしないようにしてください。マップを初期化するには、組み込みのmake関数を使用します。

m = make(map[string]int)

make関数はハッシュマップのデータ構造を割り当てて初期化し、それを指すマップ値を返します。そのデータ構造の詳細はランタイムの実装詳細であり、言語自体によって指定されていません。この記事では、マップの使用法に焦点を当て、その実装には焦点を当てません。

マップの操作

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、存在しない場合はfalsebool型です。

値を検索せずにキーの存在をテストするには、最初の値の代わりにアンダースコアを使用します。

_, 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であることを思い出してください)。この例では、Nodeのリンクリストを走査し、その値を出力します。リスト内のサイクルを検出するために、Nodeポインタのマップを使用します。

    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が訪問済みであればtruenが存在しない場合はfalseです。マップ内のnの存在をテストするために2値形式を使用する必要はありません。ゼロ値のデフォルトがそれを行ってくれます。

便利なゼロ値の別の例は、スライスのマップです。nilスライスに追記するだけで新しいスライスが割り当てられるため、スライスのマップに値を追加するのは1行で済みます。キーが存在するかどうかを確認する必要はありません。以下の例では、Person値でpeopleスライスが埋められます。各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スライスを長さゼロのスライスとして扱うため、これらの最後の2つの例は、誰もチーズやベーコンを好きでなくても機能します(ただし、そのような可能性は低いですが)。

キーの型

前述のとおり、マップのキーは比較可能な任意の型にすることができます。言語仕様でこれが正確に定義されていますが、要するに、比較可能な型とは、ブール、数値、文字列、ポインタ、チャネル、インターフェースの型、およびそれらの型のみを含む構造体または配列です。リストから特筆すべきは、スライス、マップ、関数が欠落していることです。これらの型は==を使用して比較できず、マップのキーとして使用することはできません。

文字列、int、その他の基本型がマップキーとして利用できるのは当然ですが、構造体キーは意外かもしれません。構造体は、複数の次元でデータをキー付けするために使用できます。たとえば、このマップのマップは、国ごとのウェブページのヒット数を集計するために使用できます。

hits := make(map[string]map[string]int)

これは文字列から(stringからintへのマップ)へのマップです。外部マップの各キーは、独自の内部マップを持つウェブページへのパスです。各内部マップキーは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)

ベトナム人がホームページにアクセスしたとき、適切なカウンターをインクリメント(および場合によっては作成)するのは1行で済みます。

hits[Key{"/", "vn"}]++

そして、スイス人が仕様を何人読んだかを調べるのも同様に簡単です。

n := hits[Key{"/ref/spec", "ch"}]

並行処理

マップは並行使用に対して安全ではありません。同時に読み書きした場合に何が起こるかは定義されていません。並行実行中のゴルーチンからマップの読み書きを行う必要がある場合、アクセスは何らかの同期メカニズムによって調整されなければなりません。マップを保護する一般的な方法の1つは、sync.RWMutexを使用することです。

このステートメントは、マップと埋め込みsync.RWMutexを含む匿名構造体であるcounter変数を宣言します。

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])
}

次の記事:Goミートアップに参加しよう
前の記事:go fmt your code
ブログインデックス