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) }
- 各行を
:
を区切り文字としてKeyとValueに分けたあと構造体KV
に一旦詰めて構造化されたデータに変換する - reflectionを使用して変換したい構造体フィールド毎にKVからフィールド名をキーとして持つ値をすべて集めてきて
map[string]interface{}
に詰める map[string]interface{}
の変数をjson.Marshal
により構造体に変換する- 構造体からJSONへ変換する
という工程を踏んで目的の変換を行うようにしました。
この変換処理の肝はParam2
のような複数行を[]string
に変換するところで、直接変換することが容易でないと考えて一旦構造体KVへ変換したことと、変換したい構造体とKVの値からreflectionを使用してフィールドの型ごとに値を取り出してmap[string]interface{}
型の変数に変換するようにしたところです。
まとめ
reflection自体は今回のように動的な処理を行う上では強力な武器となりますが、一方で可読性の問題などから用途としては最終手段的なものであるという認識のため使用する場合は慎重に行うべきですが、今回のような独自フォーマットの文字列を扱うような場合には使用する方法しか自分は考えつきませんでしたが、reflectionの基本的な使い方については学ぶことができたのでその点については良かったと感じています。