Skip to content

Go マップ

一般的に、マップデータ構造の実装には主に 2 種類あります。ハッシュテーブル(hash table)と検索木(search tree)です。違いは前者が順序なしで、後者が順序ありということです。Go では、map の実装はハッシュバケット(これもハッシュテーブルの一種)に基づいているため、順序はありません。この記事では実装原理について詳しくは説明しません。これは基礎の範囲を超えており、後続で深入り分析します。

TIP

マップの原理については マップ実装 をご覧ください。

初期化

Go では、マップのキー型は比較可能である必要があります。例えば stringint は比較可能ですが、[]int は比較できないため、マップのキーとして使用できません。マップを初期化するには 2 つの方法があります。1 つ目はリテラルで、形式は以下の通りです。

go
map[keyType]valueType{}

いくつか例を挙げます。

go
mp := map[int]string{
   0: "a",
   1: "a",
   2: "a",
   3: "a",
   4: "a",
}

mp := map[string]int{
   "a": 0,
   "b": 22,
   "c": 33,
}

2 つ目の方法は組み込み関数 make を使用することです。マップの場合、2 つのパラメータを受け取り、それぞれ型と初期容量です。例を以下に示します。

go
mp := make(map[string]int, 8)

mp := make(map[string][]int, 10)

マップは参照型であり、ゼロ値または初期化されていないマップはアクセスできますが、要素を格納できません。そのため、メモリを割り当てる必要があります。

go
func main() {
   var mp map[string]int
   mp["a"] = 1
   fmt.Println(mp)
}
panic: assignment to entry in nil map

TIP

マップを初期化する際は、適切な容量を割り当てることを推奨します。これにより拡張回数を削減できます。

アクセス

マップへのアクセスは、配列にインデックスでアクセスするのと同じ方法です。

go
func main() {
  mp := map[string]int{
    "a": 0,
    "b": 1,
    "c": 2,
    "d": 3,
  }
  fmt.Println(mp["a"])
  fmt.Println(mp["b"])
  fmt.Println(mp["d"])
  fmt.Println(mp["f"])
}
0
1
3
0

コードから、マップに "f" というキーが存在しなくても、戻り値があることがわかります。マップに存在しないキーの場合、戻り値は対応する型のゼロ値です。また、マップにアクセスする際、実際には 2 つの戻り値があります。1 つ目の戻り値は対応する型の値で、2 つ目の戻り値はブール値で、キーが存在するかどうかを表します。例を以下に示します。

go
func main() {
   mp := map[string]int{
      "a": 0,
      "b": 1,
      "c": 2,
      "d": 3,
   }
   if val, exist := mp["f"]; exist {
      fmt.Println(val)
   } else {
      fmt.Println("キーが存在しません")
   }
}

マップの長さを求めます。

go
func main() {
   mp := map[string]int{
      "a": 0,
      "b": 1,
      "c": 2,
      "d": 3,
   }
   fmt.Println(len(mp))
}

値の保存

マップに値を保存する方法も配列に値を保存するのと似ています。例を以下に示します。

go
func main() {
   mp := make(map[string]int, 10)
   mp["a"] = 1
   mp["b"] = 2
   fmt.Println(mp)
}

既存のキーを使用して値を保存すると、元の値が上書きされます。

go
func main() {
   mp := make(map[string]int, 10)
   mp["a"] = 1
   mp["b"] = 2
   if _, exist := mp["b"]; exist {
      mp["b"] = 3
   }
   fmt.Println(mp)
}

ただし、特別な場合もあります。キーが math.NaN() の場合です。

go
func main() {
  mp := make(map[float64]string, 10)
  mp[math.NaN()] = "a"
  mp[math.NaN()] = "b"
  mp[math.NaN()] = "c"
  _, exist := mp[math.NaN()]
  fmt.Println(exist)
  fmt.Println(mp)
}
false
map[NaN:c NaN:a NaN:b]

結果から、同じキー値が上書きされず、複数存在できること、存在するかどうかを判断できず、正常に値を取得できないことがわかります。NaN は IEE754 標準で定義されており、その実装は底层のアセンブリ命令 UCOMISD によって完了します。これは順序なしの倍精度浮動小数点数比較命令で、NaN の状況を考慮します。そのため、どの数字も NaN に等しくなく、NaN も自身に等しくありません。これにより、毎回ハッシュ値が異なってしまいます。この点についてコミュニティでも激しい議論がありましたが、公式は修正の必要はないと考えているため、NaN をマップのキーとして使用することは避けるべきです。

削除

go
func delete(m map[Type]Type1, key Type)

キーと値のペアを削除するには、組み込み関数 delete を使用します。例を以下に示します。

go
func main() {
   mp := map[string]int{
      "a": 0,
      "b": 1,
      "c": 2,
      "d": 3,
   }
   fmt.Println(mp)
   delete(mp, "a")
   fmt.Println(mp)
}
map[a:0 b:1 c:2 d:3]
map[b:1 c:2 d:3]

注意すべき点は、値が NaN の場合、そのキーと値のペアを削除できないことです。

go
func main() {
   mp := make(map[float64]string, 10)
   mp[math.NaN()] = "a"
   mp[math.NaN()] = "b"
   mp[math.NaN()] = "c"
   fmt.Println(mp)
   delete(mp, math.NaN())
   fmt.Println(mp)
}
map[NaN:c NaN:a NaN:b]
map[NaN:c NaN:a NaN:b]

トラバース

for range を使用してマップをトラバースできます。例を以下に示します。

go
func main() {
   mp := map[string]int{
      "a": 0,
      "b": 1,
      "c": 2,
      "d": 3,
   }
   for key, val := range mp {
      fmt.Println(key, val)
   }
}
c 2
d 3
a 0
b 1

結果が順序通りでないことがわかります。これはマップが順序なしで保存されることを裏付けています。值得一提的是、NaN は正常に取得できませんが、トラバースを通じてアクセスできます。例を以下に示します。

go
func main() {
   mp := make(map[float64]string, 10)
   mp[math.NaN()] = "a"
   mp[math.NaN()] = "b"
   mp[math.NaN()] = "c"
   for key, val := range mp {
      fmt.Println(key, val)
   }
}
NaN a
NaN c
NaN b

クリア

Go1.21 之前、マップをクリアするには、マップの各キーに対して delete を実行する必要がありました。

go
func main() {
  m := map[string]int{
    "a": 1,
    "b": 2,
  }
  for k, _ := range m {
    delete(m, k)
  }
  fmt.Println(m)
}

ただし、Go1.21 で clear 関数が更新されたため、以前の操作を行う必要がなくなりました。clear 1 つでクリアできます。

go
func main() {
  m := map[string]int{
    "a": 1,
    "b": 2,
  }
  clear(m)
  fmt.Println(m)
}

出力

map[]

Set

Set は順序なしで重複要素を含まないコレクションです。Go は類似のデータ構造実装を提供していませんが、マップのキーは順序なしで重複できないため、マップを使用して Set を代替できます。

go
func main() {
  set := make(map[int]struct{}, 10)
  for i := 0; i < 10; i++ {
    set[rand.Intn(100)] = struct{}{}
  }
  fmt.Println(set)
}
map[0:{} 18:{} 25:{} 40:{} 47:{} 56:{} 59:{} 81:{} 87:{}]

TIP

空の構造体はメモリを占有しません。

注意

マップは並行セーフなデータ構造ではありません。Go チームは、マップの使用の大多数の場合、高並行のシナリオは含まれないと考えており、ミューテックスロックを導入するとパフォーマンスが大幅に低下すると考えています。マップ内部には読み書き検出メカニズムがあり、競合すると fatal error がトリガーされます。例えば、以下の状況では fatal がトリガーされる可能性が非常に高いです。

go
func main() {

   group.Add(10)
   // マップ
   mp := make(map[string]int, 10)
   for i := 0; i < 10; i++ {
      go func() {
         // 書き込み操作
         for i := 0; i < 100; i++ {
            mp["helloworld"] = 1
         }
         // 読み取り操作
         for i := 0; i < 10; i++ {
            fmt.Println(mp["helloworld"])
         }
         group.Done()
      }()
   }
   group.Wait()
}
fatal error: concurrent map writes

このような状況では、sync.Map を使用して代替する必要があります。

Golang by www.golangdev.cn edit