title: '【転載】構造体を map [string] interface {} に変換するいくつかの方法'
date: 2021-08-09 16:34:33
comment: false
toc: true
category:
- Golang
tags: - 転載
- Go
- マップ
- 構造体
- 文字列
- インターフェース
- 方法
この記事は以下から転載されています:構造体を map [string] interface {} に変換するいくつかの方法 | 李文周のブログ
この記事では、Go 言語において構造体をmap[string]interface{}
に変換する際に理解しておくべき「落とし穴」と、知っておくべきいくつかの方法について説明します。
Go 言語では通常、構造体を使用してデータを保存します。たとえば、ユーザー情報を保存するために、以下のような構造体を定義することがあります。
// UserInfo ユーザー情報
type UserInfo struct {
Name string `json:"name"`
Age int `json:"age"`
}
u1 := UserInfo{Name: "q1mi", Age: 18}
今、上記のu1
をmap[string]interface{}
に変換したいとします。どうすればよいでしょうか?
構造体を map [string] interface {} に変換する#
JSON シリアライズ方式#
これはとても簡単ではないでしょうか?u1 を JSON シリアライズしてから、マップにデシリアライズすればいいのです。さっそくやってみましょう。コードは以下の通りです。
func main() {
u1 := UserInfo{Name: "q1mi", Age: 18}
b, _ := json.Marshal(&u1)
var m map[string]interface{}
_ = json.Unmarshal(b, &m)
for k, v := range m{
fmt.Printf("key:%v value:%v\n", k, v)
}
}
出力:
key:name value:q1mi
key:age value:18
見たところ問題はなさそうですが、実はここには「落とし穴」があります。それは、Go 言語のjson
パッケージが空のインターフェースに格納された数値型(整数型、浮動小数点型など)をすべて float64 型にシリアライズするということです。
つまり、上記の例でm["age"]
は現在、内部的にはfloat64
になっており、int
ではなくなっています。これを確認してみましょう:
func main() {
u1 := UserInfo{Name: "q1mi", Age: 18}
b, _ := json.Marshal(&u1)
var m map[string]interface{}
_ = json.Unmarshal(b, &m)
for k, v := range m{
fmt.Printf("key:%v value:%v value type:%T\n", k, v, v)
}
}
出力:
key:name value:q1mi value type:string
key:age value:18 value type:float64
明らかに、予期しない動作が発生しました。これを回避する方法を考える必要があります。
リフレクション#
仕方がないので、自分で実装する必要があります。ここでは、リフレクションを使用して構造体のフィールドを走査し、マップを生成する方法を示します。具体的なコードは以下の通りです。
// ToMap 構造体をMap[string]interface{}に変換する
func ToMap(in interface{}, tagName string) (map[string]interface{}, error){
out := make(map[string]interface{})
v := reflect.ValueOf(in)
if v.Kind() == reflect.Ptr {
v = v.Elem()
}
if v.Kind() != reflect.Struct { // 構造体でない場合はエラーを返す
return nil, fmt.Errorf("ToMapは構造体または構造体ポインタのみを受け付けます; %Tを取得しました", v)
}
t := v.Type()
// 構造体のフィールドを走査
// 指定されたtagNameの値をマップのキー、フィールドの値をマップの値とする
for i := 0; i < v.NumField(); i++ {
fi := t.Field(i)
if tagValue := fi.Tag.Get(tagName); tagValue != "" {
out[tagValue] = v.Field(i).Interface()
}
}
return out, nil
}
確認してみましょう:
m2, _ := ToMap(&u1, "json")
for k, v := range m2{
fmt.Printf("key:%v value:%v value type:%T\n", k, v, v)
}
出力:
key:name value:q1mi value type:string
key:age value:18 value type:int
今回はmap["age"]
の型が正しくなりました。
サードパーティライブラリ structs#
自分で実装する以外にも、GitHub には既成のライブラリがあります。たとえば、サードパーティライブラリ:https://github.com/fatih/structs。
このライブラリでは、カスタム構造体タグとしてstructs
を使用します。
// UserInfo ユーザー情報
type UserInfo struct {
Name string `json:"name" structs:"name"`
Age int `json:"age" structs:"age"`
}
使い方はとても簡単です:
m3 := structs.Map(&u1)
for k, v := range m3 {
fmt.Printf("key:%v value:%v value type:%T\n", k, v, v)
}
structs
パッケージには他にも多くの使用例がありますので、ドキュメントを確認してみてください。ただし、現在このライブラリは作者によって読み取り専用に設定されていることに注意してください。
ネストされた構造体を map [string] interface {} に変換する#
structs
はネストされた構造体をmap[string]interface{}
に変換することもサポートしており、構造体がネストされている場合、map[string]interface{}
のネストされたmap[string]interface{}
の形式に変換されます。
以下のようにネストされた構造体のセットを定義します:
// UserInfo ユーザー情報
type UserInfo struct {
Name string `json:"name" structs:"name"`
Age int `json:"age" structs:"age"`
Profile `json:"profile" structs:"profile"`
}
// Profile プロファイル情報
type Profile struct {
Hobby string `json:"hobby" structs:"hobby"`
}
構造体変数 u1 を宣言します:
u1 := UserInfo{Name: "q1mi", Age: 18, Profile: Profile{"双色球"}}
サードパーティライブラリ structs#
コードは上記と実際には同じです:
m3 := structs.Map(&u1)
for k, v := range m3 {
fmt.Printf("key:%v value:%v value type:%T\n", k, v, v)
}
出力結果:
key:name value:q1mi value type:string
key:age value:18 value type:int
key:profile value:map[hobby:双色球] value type:map[string]interface {}
結果を見ると、最後のネストされたフィールドprofile
はmap[string]interface{}
であり、マップがネストされたマップになっています。
リフレクションを使用して単層マップに変換する#
もしネストされた構造体を単層のマップに変換したい場合、どうすればよいでしょうか?
上記のリフレクションのコードを少し修正すればできます:
// ToMap2 構造体を単層マップに変換する
func ToMap2(in interface{}, tag string) (map[string]interface{}, error) {
// 現在の関数は構造体タイプのみを受け付けます
v := reflect.ValueOf(in)
if v.Kind() == reflect.Ptr { // 構造体ポインタ
v = v.Elem()
}
if v.Kind() != reflect.Struct {
return nil, fmt.Errorf("ToMapは構造体または構造体ポインタのみを受け付けます; %Tを取得しました", v)
}
out := make(map[string]interface{})
queue := make([]interface{}, 0, 1)
queue = append(queue, in)
for len(queue) > 0 {
v := reflect.ValueOf(queue[0])
if v.Kind() == reflect.Ptr { // 構造体ポインタ
v = v.Elem()
}
queue = queue[1:]
t := v.Type()
for i := 0; i < v.NumField(); i++ {
vi := v.Field(i)
if vi.Kind() == reflect.Ptr { // 内部ポインタ
vi = vi.Elem()
if vi.Kind() == reflect.Struct { // 構造体
queue = append(queue, vi.Interface())
} else {
ti := t.Field(i)
if tagValue := ti.Tag.Get(tag); tagValue != "" {
// マップに格納
out[tagValue] = vi.Interface()
}
}
break
}
if vi.Kind() == reflect.Struct { // 内部構造体
queue = append(queue, vi.Interface())
break
}
// 一般フィールド
ti := t.Field(i)
if tagValue := ti.Tag.Get(tag); tagValue != "" {
// マップに格納
out[tagValue] = vi.Interface()
}
}
}
return out, nil
}
テストしてみましょう:
m4, _ := ToMap2(&u1, "json")
for k, v := range m4 {
fmt.Printf("key:%v value:%v value type:%T\n", k, v, v)
}
出力:
key:name value:q1mi value type:string
key:age value:18 value type:int
key:hobby value:双色球 value type:string
これでネストされた構造体を単層のマップに変換することができましたが、このシナリオでは構造体とネストされた構造体のフィールドが重複しないように注意する必要があります。