まえがき
Go言語におけるmap
は、型安全性やパフォーマンスを考慮して設計されています。
そのため、map
のネスト(map
内にmap
を含む構造)など複雑なデータ構造を扱う際には、少し注意が必要です。
この記事では、map
の基本的な使い方から、応用的な使い方、注意点まで幅広く解説していきます。
mapの基本
mapの作成について
map
とは、キーと値のペアを保存するためのデータ構造です。Go言語では、map
を作成するために組み込み関数のmake
を使用します。make
関数を使うことで、新しいmap
が生成されます。
make
関数を使う際には、以下の二つの引数を指定します。
- 第1引数: 作成する
map
の型を指定します。この型には、map
が受け付けるキーの型と値の型を指定します。例えば、キーが文字列で値が整数のmap
を作る場合はmap[string]int
となります。 - 第2引数:
map
の初期サイズを指定します。これはオプションで、指定しなくても構いません。しかし、事前にどれくらいのサイズが必要かわかっている場合は、この引数を指定することでメモリ使用の効率が良くなり、パフォーマンスが向上する可能性があります。指定しない場合、または0を指定した場合、map
は自動的にサイズを調整しますが、これには追加のメモリ割り当てが発生する可能性があります。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| package main
import "fmt"
func main() {
// キーがstring型、値がint型のmapを作成
// ここでは初期サイズを5に設定しています。
scores := make(map[string]int, 5)
// mapに値を追加
scores["Alice"] = 92
scores["Bob"] = 89
// mapから値を取得して表示
fmt.Println("Aliceのスコア:", scores["Alice"])
fmt.Println("Bobのスコア:", scores["Bob"])
}
|
このコードでは、make
関数を使ってscores
という名前のmap
を作成しています。このmap
では、キーとして文字列(string
)を、値として整数(int
)を使用します。また、初期サイズを5に設定していますが、これはパフォーマンスの最適化のための一例です。scores
には、“Alice"と"Bob"という2つのキーに対応する値が追加されています。
このように、make
関数を使用することで、必要なサイズと型を指定してmap
を効率的に作成することができます。
mapから特定のキーを削除する方法
map
から特定のキーとそれに関連する値を削除するには、組み込み関数のdelete
を使用します。
コード例の解説:
以下のコードは、string
型のキーとint
型の値を持つmap
に値を追加し、その後で特定のキーを削除する方法を示しています。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| package main
import "fmt"
func main() {
// キーがstring型、値がint型のmapを作成
scores := make(map[string]int, 5)
// mapに値を追加
scores["Alice"] = 92
scores["Bob"] = 89
scores["BoB"] = 89
// "BoB"というキーを削除
delete(scores, "BoB")
// 削除後のmapを表示
fmt.Println(scores)
}
|
"BoB"
というキーとそれに関連する値89
がmap
から削除されていることがわかります。
delete
関数の第1引数には操作対象のmap
を、第2引数には削除したいキーを指定します。
delete
関数を使用する際の注意点として、指定したキーがmap
内に存在しない場合でも、エラーが発生することはありません。
つまり、安全にキーを削除できるということです。
このようにdelete
関数を使ってmap
から不要なデータを削除することは、データの整理やメモリ使用量の最適化に役立ちます。
map
を使用する際には、このようなデータの管理方法も覚えておくと良いでしょう。
mapから全てのキーを削除する方法
mapから全てのキーを削除する場合は組み込み関数のclear
を使用します。(Go1.21で実装)
以下のコードは、string
型のキーとint
型の値を持つmap
に値を追加し、その後にすべてのキーを削除する方法を示しています。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
| package main
import (
"fmt"
"os"
"runtime"
"strings"
)
func main() {
// Go1.21未満はエラー
currentVersion := runtime.Version()
if strings.Compare(currentVersion, "go1.21") < 0 {
fmt.Printf("エラー: 現在のバージョンは%sです。'clear' 関数はGo 1.21未満では未実装です。Go 1.21以降にアップグレードしてください。\n", currentVersion)
os.Exit(1)
}
// キーがstring型、値がint型のmapを作成
scores := make(map[string]int, 5)
// mapに値を追加
scores["Alice"] = 92
scores["Bob"] = 89
scores["BoB"] = 89
// すべてのキーを削除
clear(scores)
// 削除後のmapを表示
fmt.Println(scores)
// nilのmapに対してclear関数を呼び出す
var nilMap map[string]int
clear(nilMap) // 何も実行されない(エラーにならない)
fmt.Println(nilMap)
}
|
clear
関数を実行すると、すべてのキーが削除されnil
になっていることが分かります。
またnil
のmap
に対してclear
関数を使用してもエラーにはならず、何もしないことが分かります。
Go1.21未満のバージョンではdelete
関数で1つずつ削除する方法しかありませんでしたが、これで実装が楽になりますね。
色々な型のmapの使用例
map[int]stringについて
キーに整数型(int
)、値に文字列型(string
)を使った例を見ていきましょう。
コード例の解説:
以下のコードは、猫の種類を整数のキーと関連付けて保存するmap
の作成と利用方法を示しています。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| package main
import "fmt"
func main() {
// int型のキーとstring型の値を持つmapを作成
catMap := make(map[int]string)
// mapにキーと値のペアを追加
catMap[0] = "みけ猫" // 0はみけ猫
catMap[1] = "キジ猫" // 1はキジ猫
catMap[2] = "ぶち猫" // 2はぶち猫
// mapの内容を表示
fmt.Println(catMap)
}
|
このコードを実行すると、各整数キーに関連付けられた猫の種類が表示されます。実行結果は以下のようになります。
この結果からわかるように、catMap[0]
には"みけ猫”、catMap[1]
には"キジ猫"、そしてcatMap[2]
には"ぶち猫"がそれぞれ格納されています。map
のキーを使って値にアクセスすることができます。
構造体をキーとするmapについて
この例ではtime.Time
構造体をキーとして用います。
これにより、日付や時刻をキーとして特定のイベントや記念日を管理できます。
コード例の解説:
以下のコードは、特定の記念日を日付に関連付けて保存するmap
の作成と利用方法を示しています。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
| package main
import (
"fmt"
"time"
)
func main() {
// タイムゾーンを設定
Jst, err := time.LoadLocation("Asia/Tokyo")
if err != nil {
panic(err)
}
// 特定の日付をタイムゾーン付きでパース
catDay, _ := time.ParseInLocation("2006-01-02", "2022-02-22", Jst) // 猫の日
emperorsBirthday, _ := time.ParseInLocation("2006-01-02", "2022-02-23", Jst) // 天皇誕生日
springDay, _ := time.ParseInLocation("2006-01-02", "2022-03-21", Jst) // 春分の日
springDayUtc, _ := time.ParseInLocation("2006-01-02", "2022-03-21", time.UTC) // 春分の日(UTC)
// 日付をキーとするmapを作成
anniversaryDaysMap := make(map[time.Time]string)
anniversaryDaysMap[catDay] = "猫の日"
anniversaryDaysMap[emperorsBirthday] = "天皇誕生日"
anniversaryDaysMap[springDay] = "春分の日"
anniversaryDaysMap[springDayUtc] = "春分の日(UTC)"
// mapの内容を表示
fmt.Println(anniversaryDaysMap)
}
|
1
2
3
4
| map[2022-02-22 00:00:00 +0900 JST:猫の日
2022-02-23 00:00:00 +0900 JST:天皇誕生日
2022-03-21 00:00:00 +0900 JST:春分の日
2022-03-21 00:00:00 +0000 UTC:春分の日(UTC)]
|
※見やすいように改行しています
このコードでは、time.Time
構造体をキーとして使用しています。
これにより、異なるタイムゾーンを持つ同じ日付(例えば、JSTとUTCの春分の日)も、別々のキーとして扱われることが確認できます。
これは、構造体のメンバ変数が全て一致する場合にのみ、同一のキーと見なされるためです。
この例からわかるように、map
のキーとして構造体を使用することで、より複雑なキーに基づくデータの管理が可能になります。
ただし、キーとして使用する構造体は、そのメンバ変数が==
演算子で比較可能な型でなければなりません。
time.Time
構造体はこの条件を満たすため、キーとして使用できます。
interface{}をキーとするmapについて
map
のキーには通常、特定の型が指定されますが、interface{}
型をキーとして使用することで、様々な型のキーを同一のmap
内に格納することが可能になります。
コード例の解説:
以下のコードは、異なる型のキーを持つことができるmap
の作成と利用方法を示しています。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| package main
import (
"fmt"
"time"
)
func main() {
// interface{}型のキーとstring型の値を持つmapを作成
mixCatMap := make(map[interface{}]string)
// int型のキー
mixCatMap[0] = "みけ猫"
// string型のキー
mixCatMap["kiji"] = "キジ猫"
// time.Time型のキー(日付)
catDay, _ := time.Parse("2006-01-02", "2022-02-22")
mixCatMap[catDay] = "ねこの日"
// mapの内容を表示
fmt.Println(mixCatMap)
}
|
このコードを実行すると、異なる型のキー(整数、文字列、日付)を持つ要素が一つのmap
に格納され、それぞれに対応する値が表示されます。実行結果は以下のようになります。
1
| map[0:みけ猫 kiji:キジ猫 2022-02-22 00:00:00 +0000 UTC:ねこの日]
|
interface{}
型をキーとして使用することの利点は、非常に柔軟なデータ構造を作成できることです。しかし、この柔軟性は、型安全性の低下や実行時の型チェックによるパフォーマンスの低下を招く可能性があります。実際の開発では、必要な場合を除き、より具体的な型をキーとして使用することが推奨されます。
int型ポインタをキーとするmapについて
普通、map
のキーには単純な型(例えば、int
やstring
など)を使用しますが、ポインタをキーとして使用することもできます。
ここでは、int
型のポインタをキーとして使用する例を見てみましょう。
コード例の解説:
以下のコードは、int
型の変数のアドレス(つまりポインタ)をキーとして、それぞれ異なる種類の猫を表す文字列を値として持つmap
を作成しています。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| package main
import "fmt"
func main() {
// *int型のキーとstring型の値を持つmapを作成
catPointerMap := make(map[*int]string)
// int型の変数を宣言
mike := 0
buti := 1
kiji := 2
// 各変数のアドレスをキーとして、mapに値を追加
catPointerMap[&mike] = "みけ猫"
catPointerMap[&kiji] = "キジ猫"
catPointerMap[&buti] = "ぶち猫"
// mapの内容を表示
fmt.Println(catPointerMap)
}
|
このコードを実行すると、メモリ上のアドレスをキーとして持つmap
の内容が表示されます。
ただし、実際のアドレスは実行する環境によって異なります。
1
| map[0xc000130048:みけ猫 0xc000130050:キジ猫 0xc000130058:ぶち猫]
|
ポインタをキーとして使用する場合の特徴は、変数の値ではなく、その変数がメモリ上に存在する場所(アドレス)に基づいてキーが決まる点です。
これにより、たとえ変数の値が同じであっても、異なる変数(つまり異なるアドレスを持つ)であれば、異なるキーとしてmap
に追加することができます。
ただし、このようにポインタをキーとして使用することは一般的ではなく、コードの理解を難しくする可能性があります。
特に、ポインタの値(メモリアドレス)はプログラムの実行ごとに変わるため、予測不可能な振る舞いをすることがあります。
そのため、ポインタをキーとするmap
の使用は、特定の高度な用途に限定されるべきで、一般的なプログラミングでは避けた方が良いでしょう。
ネストしたmapを使ったデータのグループ化
map
をネストさせることで、複雑なデータ構造を作成することができます。
この例では、string
型のキーを持つmap
の中に、さらにstring
型のキーと値を持つmap
を格納しています。
これにより、猫と犬のデータをグループ化して管理することができます。
コード例の解説:
以下のコードは、猫と犬の種類を別々に管理するためのネストしたmap
の作成と利用方法を示しています。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| package main
import "fmt"
func main() {
// 最外層のmapを作成。キーはstring型、値は別のmap[string]string型
catDogMap := make(map[string]map[string]string)
// 猫と犬のグループを初期化
catDogMap["猫"] = make(map[string]string)
catDogMap["犬"] = make(map[string]string)
// 猫のグループに種類を追加
catDogMap["猫"]["みけ"] = "みけ猫"
catDogMap["猫"]["ぶち"] = "ぶち猫"
// 犬のグループに種類を追加
catDogMap["犬"]["柴"] = "柴犬"
catDogMap["犬"]["秋田"] = "秋田犬"
// mapの内容を表示
fmt.Println(catDogMap["猫"])
fmt.Println(catDogMap["犬"])
}
|
このコードを実行すると、以下のようにmap
の内容が表示されます。
1
2
| map[ぶち:ぶち猫 みけ:みけ猫]
map[柴:柴犬 秋田:秋田犬]
|
ここでポイントは、ネストしたmap
(この例ではcatDogMap["猫"]
やcatDogMap["犬"]
)に値を代入する前に、そのmap
自体をmake
関数で初期化する必要があることです。
make
関数を呼び出さずにネストしたmap
にアクセスしようとすると、nil map
に対する代入とみなされ、実行時にassignment to entry in nil map
エラーが発生します。
このようにネストしたmap
を使うことで、データを複数のレベルでグループ化し、より整理された形で管理することが可能になります。
ただし、ネストが深くなるほどコードの複雑性も増すため、適切な設計を心がける必要があります。
1次元配列からネストしたmapを作成する方法
この例では、猫と犬を表す構造体の配列から、ネストしたmap
をループ処理で作成する方法を紹介します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
| package main
import "fmt"
type Animal struct {
AnimalKind string
Kind string
Feature string
}
func main() {
animals := []Animal{
{AnimalKind: "猫", Kind: "三毛猫", Feature: "三毛猫は幸運の象徴とされることが多い。"},
{AnimalKind: "猫", Kind: "ぶち猫", Feature: "ぶち猫は野性味があるとされ、活発な性格のものが多い。"},
{AnimalKind: "犬", Kind: "柴犬", Feature: "柴犬は日本原産の犬種で、忠実で勇敢。"},
{AnimalKind: "犬", Kind: "秋田犬", Feature: "秋田犬は大型犬であり、忠誠心が強いとされる。"},
}
animalMap := make(map[string]map[string]string)
for _, animal := range animals {
_, ok := animalMap[animal.AnimalKind]
if !ok {
animalMap[animal.AnimalKind] = make(map[string]string)
}
animalMap[animal.AnimalKind][animal.Kind] = animal.Feature
}
fmt.Println(animalMap)
}
|
1
2
| map[犬:map[柴犬:柴犬は日本原産の犬種で、忠実で勇敢。 秋田犬:秋田犬は大型犬であり、忠誠心が強いとされる。]
猫:map[ぶち猫:ぶち猫は野性味があるとされ、活発な性格のものが多い。 三毛猫:三毛猫は幸運の象徴とされることが多い。]]
|
※見やすいように改行しています
コード例の解説:
猫と犬のデータを格納するための構造体Animal
を定義します。
この構造体には、動物の種類(AnimalKind
)、具体的な種(Kind
)、そして特徴(Feature
)のフィールドが含まれます。
Animal
構造体のスライスを作成し、いくつかの動物データを初期化しますが、ここで格納されるのは各動物の特徴です。
このスライスからネストしたmap
を作成します。
最外層のmap
は動物の種類をキーとし、内側のmap
は具体的な種をキーとして、その動物の特徴を値として格納します。
ループ内で、animalMap
にAnimalKind
をキーとするエントリが存在するかどうかをチェックします。
存在しない場合は、そのキーで新しい内側のmap
を作成し、初期化します。
次に、内側のmap
にKind
をキーとしてFeature
を値として追加します。
この方法により、動物の種類ごとに、それらの具体的な種とその特徴を効率的に整理し、アクセスすることが可能になります。
このようなデータ構造は、動物に関する情報をカテゴリ別にグループ化し、管理する際に特に便利です。
mapのキーに指定できる型と指定できない型
ここまで色々な型をmapのキーに指定してきたので、もはや何でも指定できそうですが、そうもいきません。
先述した通りmapのキーに指定できるものは== 演算子で比較可能な下記の型に限ります。
ブール型、数値型、文字列型、ポインタ型、チャネル型、インターフェイス型、およびこれらの型のみを含む構造体または配列です。
引用元:Go maps in action - Key types
mapのキーに指定できないものは下記の型になります。
スライス、マップ、および関数
引用元:Go maps in action - Key types
構造体のメンバ変数にmapのキーとして指定できない型が含まれているとinvalid map key type 〇〇
エラーとなります。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| package main
type Cat struct {
Id int
}
type CatError struct {
Id int
AtributteMap map[int]string
}
func main() {
// OK
m := make(map[Cat]string)
m[Cat{}] = "みけ猫"
//NG:Catのメンバ変数にmap型が入っているためキーにできない(invalid map key type CatError)
//m2 := make(map[CatError]string)
//m2[CatError{}] = "みけ猫"
}
|
構造体をmapのキーにしようと思ったときに、スライス、マップ、関数のいずれかがメンバ変数にあるとキーに指定できなくなるため、構造体のデータ設計は慎重に行いましょう。
2022/4/2追記
mapのキーに指定できるデータ型の集合を定義したインターフェースcopmparable
がGo 1.18で追加されました。
詳しくはGo 1.18のジェネリクスを使ってみる - comparableについて
の記事をお読みください。
mapでループすると毎回順番が違う
Go言語のmap
でループを行うと毎回要素が異なる順序で取り出される点に注意が必要です。
これは、map
の順序が保証されないためであり、不具合ではなく設計上の意図によるものです。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| package main
import "fmt"
func main() {
// int型のキーとstring型の値を持つmapを作成
catMap := make(map[int]string)
// mapにキーと値のペアを追加
catMap[0] = "みけ猫" // 0はみけ猫
catMap[1] = "キジ猫" // 1はキジ猫
catMap[2] = "ぶち猫" // 2はぶち猫
// mapの内容を表示
for _,cat := range catMap {
fmt.Println(cat)
}
}
|
このコードは実行すると最初に代入した順序と違った出力結果を出すときがあります。
これは不具合ではなく意図的にランダムになるように実装されています。
以下に、その背景を簡潔に説明します。
順序が保証されない理由
パフォーマンスの最適化
map
の主な使用目的は、キーを基に高速に値を検索することにあります。
順序を保持しようとすると、内部的に追加のデータ構造を維持する必要が出てきます。
これは、挿入、削除、検索の各操作のパフォーマンスに影響を与える可能性があります。
Go言語の設計者たちは、mapの操作を可能な限り効率的に保つことを選択しました。
シンプルな実装
順序を保証しないことで、map
の実装がシンプルになり、より堅牢で予測可能な挙動を提供できます。
これは、Go言語の全体的な設計哲学と一致しています。
すなわち、「シンプルさとは、複雑さを追加しないこと」です。
予測不可能性の導入
Go言語のmap
が毎回異なる順序で要素を返すことは、プログラマが順序に依存したコードを書くことを防ぐための意図的な選択です。
これにより、順序依存のバグを発見しやすくなるとともに、順序を意識したプログラミングを促します。
解決策: キーのソート
map
の要素を一定の順序で処理する必要がある場合、キーのリストを取得し、それをソートした上でループ処理を行うことが一般的な解決策です。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
| package main
import (
"fmt"
"sort"
)
func main() {
// int型のキーとstring型の値を持つmapを作成
catMap := make(map[int]string)
// mapにキーと値のペアを追加
catMap[0] = "みけ猫" // 0はみけ猫
catMap[1] = "キジ猫" // 1はキジ猫
catMap[2] = "ぶち猫" // 2はぶち猫
// キーを並べ替えてループする
var keys []int
for k := range catMap {
keys = append(keys, k)
}
sort.Ints(keys)
for _, k := range keys {
fmt.Println(catMap[k])
}
}
|
上記コードを実行した結果を下記に示します。
今回はキーをソートしたスライスでループしているため、何度実行しても同じ結果になります。
この方法により、キーのソート順に応じてmap
の要素を一貫して処理することができます。
ただし、この手法を用いる際は、map
のキーの型に応じて適切なソート関数を選択する必要があります。
ちなみに上記のキーを取得して並べ替えるコードは公式のコードを今回の例で置き換えたものです。
Go maps in action - Iteration order
ネストしたmapでソートする
ネストしたmap
の場合、外側のmap
をループしてキーを取得・ソートし、その後内側のmap
に対しても同様に処理を行う必要があります。
以下の例では、ペットの種類(例: 猫、犬)ごとに名前と特徴を格納するネストしたmap
を扱います。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
| package main
import (
"fmt"
"sort"
)
func main() {
// 動物の種類ごとに名前と特徴を格納するmap
animalFeatures := make(map[string]map[string]string)
animalFeatures["2.猫"] = map[string]string{
"2.三毛猫": "三毛猫は幸運の象徴とされることが多い。",
"1.ぶち猫": "ぶち猫は野性味があるとされ、活発な性格のものが多い。",
"3.キジ猫": "賢く活発で、人懐っこい。",
}
animalFeatures["1.犬"] = map[string]string{
"2.柴犬": "柴犬は日本原産の犬種で、忠実で勇敢。",
"1.秋田犬": "秋田犬は大型犬であり、忠誠心が強いとされる。",
"3.土佐犬": "秋田犬は大型犬であり、落ち着いているが力強い。",
}
// 動物の種類を辞書順にソートして表示
var animalTypes []string
for animalType := range animalFeatures {
animalTypes = append(animalTypes, animalType)
}
sort.Strings(animalTypes)
for _, animalType := range animalTypes {
fmt.Println(animalType + "の種類と特徴:")
// 動物の名前を辞書順にソートして表示
var animalNames []string
for name := range animalFeatures[animalType] {
animalNames = append(animalNames, name)
}
sort.Strings(animalNames)
for _, name := range animalNames {
fmt.Printf(" %s: %s\n", name, animalFeatures[animalType][name])
}
}
}
|
このコードを実行すると、ペットの種類ごとに、名前がアルファベット順にソートされて表示されます。
外側のループではペットの種類(猫、犬)がソートされ、内側のループでは各種類に属するペットの名前がソートされます。
1
2
3
4
5
6
7
8
| 1.犬の種類と特徴:
1.秋田犬: 秋田犬は大型犬であり、忠誠心が強いとされる。
2.柴犬: 柴犬は日本原産の犬種で、忠実で勇敢。
3.土佐犬: 秋田犬は大型犬であり、落ち着いているが力強い。
2.猫の種類と特徴:
1.ぶち猫: ぶち猫は野性味があるとされ、活発な性格のものが多い。
2.三毛猫: 三毛猫は幸運の象徴とされることが多い。
3.キジ猫: 賢く活発で、人懐っこい。
|
この方法により、ネストしたmap
のデータを一定の順序で処理することが可能になります。
ネストが深い場合は、この処理を適切なレベルで再帰的に適用することで、任意の深さのネストに対応できます。
まとめ
私がmapを使って躓いたところを書いてみましたが、いかがでしたか。
mapはよく使うのでもう少し楽に書けたらいいのですが、そこは今後のアップデートで改善を期待したい所です。
今後もmapを使って気になる所があれば随時更新していきたいと思います。
この記事で紹介したプログラムはコピーしてGo Playground
に貼り付ければ実行できますので、実際の動作を確認してみてください。