banner
biuaxia

biuaxia

"万物皆有裂痕,那是光进来的地方。"
github
bilibili
tg_channel

【転載】構造体をmap_string_interfaceに変換するいくつかの方法

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}

今、上記のu1map[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 {}

結果を見ると、最後のネストされたフィールドprofilemap[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

これでネストされた構造体を単層のマップに変換することができましたが、このシナリオでは構造体とネストされた構造体のフィールドが重複しないように注意する必要があります。

読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。