reflectionを使用した構造体データと独自フォーマット文字列の相互変換

TL;DR

今回の記事内に出てくるコードの全体は以下レポジトリにあります

https://github.com/litencatt/playground-go/blob/main/convert-struct/main.go

出力結果

$ go run main.go
=== Struct to String Lines ===
Input:
{Param1:Val1 Param2:[Val2 Val3]}
Output:
Param1:Val1
Param2:Val2
Param2:Val3

Input:
{Param3:Val4 Param4:Val5}
Output:
Param3:Val4
Param4:Val5

=== String Lines to JSON ===
Input:
Param1:Val1
Param2:Val2
Param2:Val3

KV: [{Key:Param1 Value:Val1} {Key:Param2 Value:Val2} {Key:Param2 Value:Val3}]
Output:
{
  "Param1": "Val1",
  "Param2": [
    "Val2",
    "Val3"
  ]
}

Input:
Param3:Val4
Param4:Val5

KV: [{Key:Param3 Value:Val4} {Key:Param4 Value:Val5}]
Output:
{
  "Param3": "Val4",
  "Param4": "Val5"
}

構造体から文字列への変換

以下に示す構造体R1のような構造体を

type R1 struct {
    Param1 string
    Param2 []string
}

以下のような構造体のフィールド名と値が:で区切られた文字列として出力したい場合、どのようなやり方があるでしょうか?

Param1:Val1
Param2:Val2
Param2:Val3

今回変換する際にParam2のような配列の場合は複数行として出力したいという条件がありました。
また、構造体R1はたんなる1例であり、入力として与えられる構造体は複数のstringまたは[]string型のフィールドを持っているものとします。

そうした条件のもと変換する関数を考えたところ以下のような実装で実現することができました。 当初はreflectionを使用しないで実現できないかと検討していましたが、やはりネックなのはどのフィールドが[]string型なのかでそれを判断して処理する必要があり、最終的にreflectionを使用することとなりました。

func convToStr(s interface{}) string {
    var res string
    r := reflect.ValueOf(s)
    for i := 0; i < r.NumField(); i++ {
        t := r.Type().Field(i)
        f := r.Field(i)
        switch f.Kind() {
        case reflect.String:
            res += fmt.Sprintf("%s:%s\n", t.Name, f)
        case reflect.Slice:
            for i := 0; i < f.Len(); i++ {
                res += fmt.Sprintf("%s:%s\n", t.Name, f.Index(i))
            }
        default:
            fmt.Printf("not support kind %s", f.Kind())
        }
    }
    return res
}

文字列からJSONへの変換

続いては先程出力した文字列をR1構造体に逆変換し最終的にJSONを出力する方法を考えてみます。

Param1:Val1
Param2:Val2
Param2:Val3

こちらは文字列のパース処理となるため先程の変換処理よりもコードは増えました

type R1 struct {
    Param1 string
    Param2 []string
}

type KV struct {
    Key   string
    Value string
}

func decodeJSON(input string, payload interface{}) error {
    params := make(map[string]interface{})
    responseLines := strings.Split(input, "\n")

    var kv []KV
    for _, l := range responseLines {
        // ":"を区切り文字としてKey, Valueを抽出
        idx := strings.Index(l, ":")
        if idx == -1 {
            continue
        }
        k := l[:idx]
        v := chop(l[idx+1:])
        kv = append(kv, KV{Key: k, Value: v})
    }
    fmt.Printf("KV: %+v\n", kv)

    r := reflect.ValueOf(payload).Elem()
    for i := 0; i < r.NumField(); i++ {
        f := r.Field(i)
        t := r.Type().Field(i)
        vs := collectValues(kv, t.Name)
        if len(vs) == 0 {
            continue
        }
        switch f.Kind() {
        case reflect.Slice:
            params[t.Name] = vs
        case reflect.String:
            params[t.Name] = vs[0]
        default:
            fmt.Printf("not support kind %s", f.Kind())
        }
    }
    js, err := json.Marshal(params)
    if err != nil {
        return err
    }

    return json.Unmarshal(js, payload)
}
  1. 各行を:を区切り文字としてKeyとValueに分けたあと構造体KVに一旦詰めて構造化されたデータに変換する
  2. reflectionを使用して変換したい構造体フィールド毎にKVからフィールド名をキーとして持つ値をすべて集めてきてmap[string]interface{}に詰める
  3. map[string]interface{}の変数をjson.Marshalにより構造体に変換する
  4. 構造体からJSONへ変換する

という工程を踏んで目的の変換を行うようにしました。

この変換処理の肝はParam2のような複数行を[]stringに変換するところで、直接変換することが容易でないと考えて一旦構造体KVへ変換したことと、変換したい構造体とKVの値からreflectionを使用してフィールドの型ごとに値を取り出してmap[string]interface{}型の変数に変換するようにしたところです。

まとめ

reflection自体は今回のように動的な処理を行う上では強力な武器となりますが、一方で可読性の問題などから用途としては最終手段的なものであるという認識のため使用する場合は慎重に行うべきですが、今回のような独自フォーマットの文字列を扱うような場合には使用する方法しか自分は考えつきませんでしたが、reflectionの基本的な使い方については学ぶことができたのでその点については良かったと感じています。