👓

【Go】再帰を使ってネストの深いJSONの値を取得する

2024/03/11に公開

はじめに

再帰、使ってますか?

ちょっと読みにくいコードですが、書けるようになると選択の幅が広がります。
「再帰って何?」そんな方にも読めるよう、なるべくコメントをつけて実装したので最後まで読んで再帰を使えるようになりましょう!

再帰って何?

再帰とは、「関数が自分自身の関数を呼び出すこと」です。

例: 関数Aの中で関数Aを呼び出す
※このコードはpanicとなります。

func A() string {
	return A()
}

分からない方、まずはなんとなく理解しておくといいかもしれません。
再帰処理とは|「分かりそう」で「分からない」でも「分かった」気になれるIT用語辞典

JSONから値を取り出そう

以下のようなネストが深いJSONがあり、太郎だけを取り出したい時はどうするでしょうか。

{
  "user": {
    "name": {
      "first": {
        "kanji": "太郎",
        "kana": "たろう"
      },
      "last": {
        "kanji": "山田",
        "kana": "やまだ"
      }
    }
  }
}

1. 構造体を使ってマッピング

よくあるのが一時的な構造体を作り、そこにUnmarshalで値を入れる方法です。
シンプルで分かりやすいので、こちらも悪くないと思っています。しかし使い回すことが難しいので、複数の場所で同じようなコードが並ぶと冗長に感じます。

func getTaro() string {
	s := struct {
		User struct {
			Name struct {
				First struct {
					Kanji string `json:"kanji"`
				} `json:"first"`
			} `json:"name"`
		} `json:"user"`
	}{}

	if err := json.Unmarshal([]byte(responseJSON), &s); err != nil {
		panic(err)
	}

	return s.User.Name.First.Kanji
}

2. 再帰で取得

再帰で同じように太郎を取得してみます。

getFromMapという再帰関数を作成し、その引数にはkeyの値を配列で入れています。
この関数はmapkey[]を定義すればどれだけネストが深い構造でも、1つの値を取得することができます。
(もちろん使い回すことができます)

func getTaro2() string {
	var m map[string]any
	if err := json.Unmarshal([]byte(responseJSON), &a); err != nil {
		panic(err)
	}

	return getFromMap(m, "user", "name", "first", "kanji")
}

// 再帰関数
func getFromMap(m map[string]any, keys ...string) string {
	// キーがなければ終了(空の値を返す)
	if len(keys) == 0 {
		return ""
	}

	// 最初のキーとそれに対する値を取得する
	key := keys[0]
	value := m[key]

	// キーが1つだけの場合は目的の値の階層に到達したことになるため、
	// 型アサーションを実行して値を返す
	if len(keys) == 1 {
		v, ok := value.(string)
		if !ok {
			return ""
		}
		return v
	}

	// 1階層下のmapを取得する
	nextMap, ok := value.(map[string]any)
	if !ok {
		return ""
	}
	// 次のキーを取得する
	nextKeys := keys[1:]

	// 再帰を使って次の階層のmapを取得する
	return getFromMap(nextMap, nextKeys...)
}

以下のgitリポジトリに関数と使い方を載せているので、実際に動かして確認してみてください。

https://github.com/ShotaTotsuka/go-mapextractor

何をしているのか?

keysの配列の値をindex:0から処理し、配列の中身が1つになった時点でvalueを取得します。
keysの中身が複数ある場合は、先頭からkeyとvalueを1つ削って、再度同じ関数を呼び出します。

3. forで取得

使いまわせる関数を作成する場合、再帰よりforの方が読みやすくなることは多々あります。
再帰は1つの手段でしかありません。

以下はforでループした場合の関数です。(今回はこっちの方が良さそうかも...笑)

func getTaro3() string {
	var m map[string]any
	if err := json.Unmarshal([]byte(responseJSON), &m); err != nil {
		panic(err)
	}

	return getFromMapFor(m, "user", "name", "first", "kanji")
}

func getFromMapFor(m map[string]any, keys ...string) string {
	var res string
	mm := m
	for i, key := range keys {
		if len(keys)-1 == i {
			v, ok := mm[key].(string)
			if !ok {
				return ""
			}
			return v
		}

		newMap, ok := mm[key].(map[string]any)
		if !ok {
			return ""
		}
		mm = newMap
	}

	return res
}

さいごに

forループや構造体に定義した方が直感的で読みやすくはありますが、再帰も1つの候補として持っておくといつか活躍する日が来るかもしれません。

Discussion