JavaScriptを有効にしてください

Go言語におけるmapの使い方

 · 

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

このコードを実行すると、以下のような結果が得られます。

1
map[Alice:92 Bob:89]

"BoB"というキーとそれに関連する値89mapから削除されていることがわかります。
delete関数の第1引数には操作対象のmapを、第2引数には削除したいキーを指定します。

delete関数を使用する際の注意点として、指定したキーがmap内に存在しない場合でも、エラーが発生することはありません。
つまり、安全にキーを削除できるということです。

このようにdelete関数を使ってmapから不要なデータを削除することは、データの整理やメモリ使用量の最適化に役立ちます。
mapを使用する際には、このようなデータの管理方法も覚えておくと良いでしょう。

では、色々な型におけるmapの作成事例について解説していきます。

色々な型の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)
}

このコードを実行すると、各整数キーに関連付けられた猫の種類が表示されます。実行結果は以下のようになります。

1
map[0:みけ猫 1:キジ猫 2:ぶち猫]

この結果からわかるように、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{}型をキーとして使用することの利点は、非常に柔軟なデータ構造を作成できることです。しかし、この柔軟性は、型安全性の低下や実行時の型チェックによるパフォーマンスの低下を招く可能性があります。実際の開発では、必要な場合を除き、より具体的な型をキーとして使用することが推奨されます。

この例では、interface{}型をキーとして使用していますが、これにより任意の型のキーをmapに追加することができます。ただし、このようなmapの使用は慎重に行うべきであり、特にパフォーマンスが重要な場面では避けた方が良いでしょう。

int型ポインタをキーとするmapについて

普通、mapのキーには単純な型(例えば、intstringなど)を使用しますが、ポインタをキーとして使用することもできます。
ここでは、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
map[犬:map[柴犬:柴犬は日本原産の犬種で、忠実で勇敢。 秋田犬:秋田犬は大型犬であり、忠誠心が強いとされる。] 猫:map[ぶち猫:ぶち猫は野性味があるとされ、活発な性格のものが多い。 三毛猫:三毛猫は幸運の象徴とされることが多い。]]

コード例の解説:

猫と犬のデータを格納するための構造体Animalを定義します。
この構造体には、動物の種類(AnimalKind)、具体的な種(Kind)、そして特徴(Feature)のフィールドが含まれます。

Animal構造体のスライスを作成し、いくつかの動物データを初期化しますが、ここで格納されるのは各動物の特徴です。

このスライスからネストしたmapを作成します。
最外層のmapは動物の種類をキーとし、内側のmapは具体的な種をキーとして、その動物の特徴を値として格納します。

ループ内で、animalMapAnimalKindをキーとするエントリが存在するかどうかをチェックします。
存在しない場合は、そのキーで新しい内側のmapを作成し、初期化します。
次に、内側のmapKindをキーとして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)
	}
}
実行結果
1
2
3
キジ猫
ぶち猫
みけ猫

このコードは実行すると最初に代入した順序と違った出力結果を出すときがあります。

これは不具合ではなく意図的にランダムになるように実装されています。
以下に、その背景を簡潔に説明します。

順序が保証されない理由

パフォーマンスの最適化

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

上記コードを実行した結果を下記に示します。

1
2
3
みけ猫
キジ猫
ぶち猫

今回はキーをソートしたスライスでループしているため、何度実行しても同じ結果になります。
この方法により、キーのソート順に応じて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
package main

import (
	"fmt"
	"sort"
)

func main() {
	// 動物の種類ごとに名前と特徴を格納するmap
	animalFeatures := make(map[string]map[string]string)
	animalFeatures["猫"] = map[string]string{
		"三毛猫": "三毛猫は幸運の象徴とされることが多い。",
		"ぶち猫": "ぶち猫は野性味があるとされ、活発な性格のものが多い。",
	}
	animalFeatures["犬"] = map[string]string{
		"柴犬": "柴犬は日本原産の犬種で、忠実で勇敢。",
		"秋田犬": "秋田犬は大型犬であり、忠誠心が強いとされる。",
	}

	// 動物の種類を辞書順にソートして表示
	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
犬の種類と特徴:
  柴犬: 柴犬は日本原産の犬種で、忠実で勇敢。
  秋田犬: 秋田犬は大型犬であり、忠誠心が強いとされる。
猫の種類と特徴:
  ぶち猫: ぶち猫は野性味があるとされ、活発な性格のものが多い。
  三毛猫: 三毛猫は幸運の象徴とされることが多い。

この方法により、ネストしたmapのデータを一定の順序で処理することが可能になります。
ネストが深い場合は、この処理を適切なレベルで再帰的に適用することで、任意の深さのネストに対応できます。

まとめ

私がmapを使って躓いたところを書いてみましたが、いかがでしたか。
mapはよく使うのでもう少し楽に書けたらいいのですが、そこは今後のアップデートで改善を期待したい所です。

今後もmapを使って気になる所があれば随時更新していきたいと思います。

この記事で紹介したプログラムはコピーしてGo Playground に貼り付ければ実行できますので、実際の動作を確認してみてください。


スポンサーリンク

共有

もふもふ
著者
もふもふ
プログラマ。汎用系→ゲームエンジニア→Webエンジニア→QAエンジニア