Golang: Maps(デベロッパーコミュニティ)

はじめに

シリーズの第7部では、Maps(マップ)について取り上げます。配列やスライスといった基本的なデータ構造についての説明を終え、これからマップやハッシュテーブルについて進めます。マップは特定の型のキーと値のペアを保存するために使います。このシリーズのこのパートでは、Golangでのマップの基本、宣言、反復処理、そしてマップからのキーの作成、更新、削除について話します。

Golangにおけるマップ

Golangのマップはキーと値のペアを保存する方法を提供するデータ構造です。ハッシュテーブルとしても知られています。マップは一意のキーを作成することができ、それが値に関連付けられます。例えば、数値のリストの頻度表を作成するようなデータ構造を作るために使うことができます。リスト内で各要素が出現する頻度を保存することができます。例えば、[3, 5, 9, 4, 9, 5, 5]という数値のリストがあるとすると、これらの要素の頻度マップを[3:1, 5:3, 4:1, 9:2]のように作成することができます。ここで、キー-値のペアとして頻度情報を保存しています。つまり、3は1回、5は3回出現しています。

マップは順序が保存されないため、必要に応じて手動で並べ替える必要があります。

マップの宣言

2つの型のマッピングが定義されることによってマップを宣言することができます。文字と整数、あるいは整数同士など、どのような型もマッピングできます。マップを宣言する方法はいくつかあり、mapリテラルやmake関数、new関数などを使って声明することができます。それぞれについて簡単に見ていきます。

シンプルなマップリテラル

配列やスライスでスライスリテラルを使ったように、マップリテラルを使ってGolangでマップを作成することができます。ここではmapキーワードの後にマッピングする2つのデータ型を記述します。

package main

import "fmt"

func main() {
    char_freq := map[string]int{
        "M": 1,
        "e": 2,
        "t": 1,
    }
    fmt.Println(char_freq)
}

実行結果は次のとおりです。

$ go run map.go
map[M:1 e:2 t:1]

マップキーワードを使用して、string型をint型とマップするマップを初期化しました。最初のデータ型は角括弧[]の内側に、2番目のデータ型は角括弧の外側に宣言されます。値を定義するために{}を使用します。{}を空にすることもできます。

char_freq := map[string]int{}

この例では、データ型のデータを指定して、この例では文字列""の後にコロン:を続け、最後に2番目のペアの値を指定します。値はそれぞれコンマ(,)で区切られます。

make関数の利用

make関数を使用してGolangでマップを作成することもできます。make関数はメモリを割り当てるために使用されます。「make」関数ははじめに提供された値に対して十分なメモリを割り当て、マップのサイズが増えるにつれてより多くのメモリを割り当てます。make関数はmapキーワードとともにキーと値のデータ型を与えます。オプションでスライス宣言で提供した容量を与えることもできます。基本的には上限に達すると2倍になり再割当てされます。

marks := make(map[int]int)
marks[65] = 8
marks[95] = 3
marks[80] = 5
fmt.Println(marks)

実行結果は次のようになります。

$ go run map.go
map[65:8 80:5 95:3]

make関数を用いてマップを宣言しています。もし特に言及されていなければ、初期サイズはだいたい7になります。7に達した後、容量は主に倍になり、修正に応じて増加します。

new関数の利用

new関数(少々癖がありますが)を使ってGolangでマップを作ることもできます。new関数は基本的にメモリを割り当てるために使うのですが、「make」関数とは異なり、割り当てたポインタのメモリアドレスを返すものです。したがって、new関数の関数呼び出しの返り値をポインタ変数に設定することができます。Golangのポインタは単にメモリアドレスへの参照ですが、これについては別のセクションで詳しく触れます。ポインタがメモリアドレスに割り当てられた後、そのポインタのアドレスを参照し、それによって元の値であるマップ自体にアクセスすることができます。

name := new(map[byte]int)
*name = map[byte]int{}
name_map := *name

name_map['m'] = 1
name_map['e'] = 2
name_map['t'] = 1

fmt.Println(name_map)

for k, _ := range name_map {
    fmt.Printf("%c\n", k)
}

実行結果は以下の通りです。

$ go run map.go
map[101:2 109:1 116:1]
m
e
t

ですので、new関数でマップを作成し、そのアドレスをポインタに保存しました。その後で空のマップを初期化し、元の参照を同じポインタアドレスに保存しました。そして最後に、他の変数にマップを保存することで、通常のマップとして使用することができます。これがnew関数を使ってマップを宣言する方法です。

マップ内のキーと値へのアクセス

キーを使って簡単に値にアクセスすることができます。角括弧を使ってキーを指定すると、そのキーに関連付けられた値を取得できます。例えば、マップ["M": 1, "E": 2, "T":1]であれば、map_name["E"]を使うと3という値を取得できます。

マップの長さ

マップの長さはlen関数を使ってアクセスできます。len関数はマップ内のキーと値のペアの数を返します。

char_freq := map[string]int{
    "M": 1,
    "e": 2,
    "t": 1,
}
fmt.Println(char_freq)
fmt.Println(len(char_freq))

実行結果は次のようになります。

$ go run map.go
map[M:1 e:2 t:1]
3

マップ内の既存のキーを確認する

カンマ-ok構文を使ってマップ内にキーが存在するかどうかを確認することができます。キーは最初の変数を使ってアクセスされ、キーが存在しない場合は2番目の変数がfalseに設定されます。したがって、2つの変数のアプローチを利用してマップ内のキーの存在を確認することができます。

name_map := map[byte]int{
    'm': 1,
    'e': 2,
    't': 1,
}
var key byte = 't'
value, exist := name_map[key]
if exist == true {
    fmt.Printf("The key %c exist and has value %d\n", key, value)
} else {
    fmt.Printf("The key %c does not exist.\n", key)
}

実行結果は以下の通りです。

$ go run map.go
The key t exist and has value 1

ですので、キーが存在すればexist値はtrueとなり、存在しなければfalseとなります。このようにして、特定のキーがマップ内に存在するかどうかを確認できます。

マップ内のキー/値の追加と変更

初期化処理で行ったように、キーを使ってマップ内にキーと値のペアを追加することができます。適切なデータ型の値を割り当てるために、角括弧[]内にキーを渡します。

cart_list := map[string]int{
    "shirt": 2,
    "mug": 4,
    "shoes": 3,
}

fmt.Println(cart_list)

cart_list["jeans"] = 1
cart_list["mug"] = 3
fmt.Println(cart_list)

実行結果は次のようになります。

$ go run map.go
map[mug:4 shirt:2 shoes:3]
map[jeans:1 mug:3 shirt:2 shoes:3]

キーをそのまま使って、それが保持する値を変更することができます。キーと値のペアの追加についても同じことが言えます。関連付けられた値を使用してキーを使います。

マップでキーを削除する

delete関数を使用してマップからキーと値のペアを削除できます。

cart_list := map[string]int{
    "shirt": 2,
    "mug":   4,
    "shoes": 3,
}
fmt.Println(cart_list)

cart_list["jeans"] = 1
cart_list["mug"] = 3
delete(cart_list, "shoes")

fmt.Println(cart_list)

実行結果は以下の通りです。

$ go run map.go
map[mug:4 shirt:2 shoes:3]
map[jeans:1 mug:3 shirt:2]

マップからキーと値のペアが削除されたことがわかります。

マップを反復処理する

rangeキーワードを使ってスライスや配列の反復処理を行うのと同じように、マップでも反復処理を行うことができますが、ここでの例外は、インデックスや要素のコピーの代わりにキーと値を使用します。

is_prime := map[int]bool{
    7:  true,
    9:  false,
    13: true,
    15: false,
    16: false,
}

for key, value := range is_prime {
    fmt.Printf("%d -> %t\n", key, value)
}

実行結果は次のとおりです。

$ go run map.go
9 -> false
13 -> true
15 -> false
16 -> false
7 -> true

マップを反復処理する際にrangeキーワードを使用してキーと値にアクセスできることがわかります。forループ内でマップに存在する値を参照することができます。

反復処理中にキーまたは値のみを使用する

keyvalueのどちらかを使用しない場合、コンパイラが未使用変数のエラーを出す可能性があるので、_アンダースコア文字という代替変数を使用して対処することができます。

is_prime := map[int]bool{
    7:  true,
    9:  false,
    13: true,
    15: false,
    16: false,
}

for key, _ := range is_prime {
    fmt.Printf("Key : %d\n", key)
}

for _, value := range is_prime {
    fmt.Printf("Value: %t\n", value)
}

実行結果は次のようになります。

$ go run map.go
Key : 7
Key : 9
Key : 13
Key : 15
Key : 16
Value: true
Value: false
Value: true
Value: false
Value: false

ループ内で変数が使用されていない場合は、アンダースコア演算子を使用して完全に無視してコンパイルエラーや警告を防ぐことができます。したがって、キーのみにアクセスしたい場合はkey, _を使用し、マップの値を黙らせます。値にのみアクセスしたい場合は_, valueを使用すると、マップからすべての値を取得できます。変数名keyvalueは何でも構いませんが、ループ内でのみ使用してください。

以上でこのパートは終わりです。すべてのコード例とコマンドの参照は、100 days of Golang GitHubリポジトリで見つけることができます。

まとめ

このシリーズのこのパートから、Golangでのマップの基本について理解を深めることができました。宣言、初期化、反復処理を含む基本的な内容を扱いました。マップはシンプルですが、面白いアプリケーションを作成するためには重要です。

読んでいただきありがとうございます。ご質問やフィードバックがあれば、コメント欄やソーシャルハンドルからお知らせください。Happy Coding :)

こちらの記事はdev.toの良い記事を日本人向けに翻訳しています。
https://dev.to/mr_destructive/golang-maps-3oi6