Go 言語で map(Key-Value 型の仕組み)をサンプルコードを通して理解する

はじめに

今回のエントリではここまで作成したモジュールを変更して複数の人々に挨拶を返却したいと思います。
言い換えると「複数の入力」を受け取り、その入力とペアになる「複数の出力」を返す仕組みを実現してみます。

当エントリは以下のエントリの続きとなっています。事前に一読しておくことをおすすめします。

また、 スライス についても理解してきましょう。

Hello 関数はパラメータを 1 つしか受け取れないため修正したいが...

ここまで作成した Hello 関数のパラメータを 1 つだけの状態から複数受け取れるように変更したいとしましょう。

しかし、すでに genzouw/greetings モジュールが公開されており、 Hello 関数の呼び出し元が存在しているため、関数の入力パラメータの型が変更されると壊れてしまいます。

こんなときは、名前の異なる別関数を新規作成します。

他言語でも同様の考えではありますが、Go 言語は特に後方互換を考慮した考えを強く意識しているように感じます。

コーディング開始!

1. greetings/greetings.go を開き、コードを変更

package greetings

import (
    "errors"
    "fmt"
    "math/rand"
    "time"
)

func Hello(name string) (string, error) {
    if name == "" {
        return "", errors.New("empty name")
    }

    message := fmt.Sprintf(randomFormat(), name)

    return message, nil
}

func Hellos(names []string) (map[string]string, error) {
    messages := make(map[string]string)
    for _, name := range names {
        message, err := Hello(name)
        if err != nil {
            return nil, err
        }
        messages[name] = message
    }
    return messages, nil
}

func init() {
    rand.Seed(time.Now().UnixNano())
}

func randomFormat() string {
    formats := []string{
        "Hi, %v. Welcome!",
        "Great to see you, %v!",
        "Hail, %v! Well met!",
    }

    return formats[rand.Intn(len(formats))]
}

前回からの差分は以下になります。

$ git  diff
diff --git greetings.go greetings.go
index f4fc944..042e0f4 100644
--- greetings.go
+++ greetings.go
@@ -17,6 +17,18 @@ func Hello(name string) (string, error) {
        return message, nil
 }

+func Hellos(names []string) (map[string]string, error) {
+       messages := make(map[string]string)
+       for _, name := range names {
+               message, err := Hello(name)
+               if err != nil {
+                       return nil, err
+               }
+               messages[name] = message
+       }
+       return messages, nil
+}
+
 func init() {
        rand.Seed(time.Now().UnixNano())
 }

コード解説

かんたんに解説します。

  • Hellos 関数を追加。
    • Hello 関数では引数として「1 人の名前」を string 型で受け取る形式だったものを「複数の名前」を受け取る形式に変更
    • 戻り値はもともと string 型の値だったものを map 型に変更
  • 追加した Hellos 関数は Hello 関数を呼び出す。
    • 重複したロジックを減らす工夫となっています。
  • messages という map 型変数を作成しています。
    • この map 型変数を使い、受け取った「名前」 ( キーとして利用 ) とその名前に対する「生成された挨拶文」 ( バリューとして使用 ) をマッピングします。
  • Go 言語では、次の構文で map の初期化を行えます。
    • make(map[key-type]value-type)
  • 受け取った names パラメータでループし、 Hello 関数を呼び出します。
    • range キーワードでループさせた結果、2 つの値を取得できます。 1 つ目はスライスや配列のインデックス、2 つ目はスライスや配列の値です。
    • 今回はインデックスは不要なので、Go の ブランク識別子 ( アンダースコアのこと ) を使っています。

2. hello/hello.go を開き、 コードを変更

package main

import (
    "fmt"
    "log"

    "genzouw/greetings"
)

func main() {
    log.SetPrefix("greetings: ")
    log.SetFlags(0)

    names := []string{"genzouw", "tsubasa", "misaki"}

    messages, err := greetings.Hellos(names)
    if err != nil {
        log.Fatal(err)
    }

    fmt.Println(messages)
}

前回からの差分は以下になります。

$ git diff
diff --git hello.go hello.go
index 80f2768..b986ec1 100644
--- hello.go
+++ hello.go
@@ -11,10 +11,12 @@ func main() {
        log.SetPrefix("greetings: ")
        log.SetFlags(0)

-       message, err := greetings.Hello("genzouw")
+       names := []string{"genzouw", "tsubasa", "misaki"}
+
+       messages, err := greetings.Hellos(names)
        if err != nil {
                log.Fatal(err)
        }

-       fmt.Println(message)
+       fmt.Println(messages)
 }
  • names 変数に「複数の名前」が格納されたスライスを代入します。
  • names 変数を Hellos 関数に渡します。
  • Hellos 関数の実行結果を messages 変数に代入します。

アプリケーション実行

hello モジュールの配置されているディレクトリで、コードを実行してみます。

実行した結果出力される情報は、 名前とその名前に対するメッセージのペアが格納された map の文字列表現となっています。

以下のようなメッセージが表示されるはずです。

$ go run .
map[genzouw:Hail, genzouw! Well met! misaki:Hi, misaki. Welcome! tsubasa:Great to see you, tsubasa!]

ひとこと

今回のエントリでは、 「キー」と「値」のペアを取り扱う際に便利な map について取り上げました。

また、新機能を実装する際には新しい関数を実装することで下位互換性を崩さないようにできるということについても取り上げました。

次回は 「Go 言語組み込みのユニットテスト機能」 を触ってみます。

Posted by genzouw