This article describes the “pitfalls” you need to know when converting struct to map[string]interface{} in Go, and also some of the methods you need to know.

We usually use struct in Go to store our data, for example, to store user information, we might define the following struct.

1
2
3
4
5
6
7
// UserInfo 
type UserInfo struct {
	Name string `json:"name"`
	Age  int    `json:"age"`
}

u1 := UserInfo{Name: "q1mi", Age: 18}

struct to map[string]interface

JSON serialization method

Serialize u1 with JSON, then deserialize to map

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
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)
	}
}

Output:

1
2
key:name value:q1mi
key:age value:18

It doesn’t seem to be a problem, but there is actually a “pitfall” here. That is, the json package in the Go language serializes the numeric types (integers, floats, etc.) stored in the null interface to the float64 type.

That is, m["age"] in the above example is now a float64 at the bottom, not an int anymore. Let’s verify that.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
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)
	}
}

Output:

1
2
key:name value:q1mi value type:string
key:age value:18 value type:float64

Obviously, there was an unexpected behavior and we need to find a way to circumvent it.

Reflection

There is no way to do it yourself. Here we use reflection to generate map by traversing the structure fields as follows.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// ToMap struct to 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 {  // Non-structural return error
		return nil, fmt.Errorf("ToMap only accepts struct or struct pointer; got %T", v)
	}

	t := v.Type()
	// Traversing structure fields
	// Specify the tagName value as the key in the map; the field value as the value in the map
	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
}

Validate:

1
2
3
4
m2, _ := ToMap(&u1, "json")
for k, v := range m2{
	fmt.Printf("key:%v value:%v value type:%T\n", k, v, v)
}

Output:

1
2
key:name value:q1mi value type:string
key:age value:18 value type:int

This time the type of map[“age”] is correct.

Third-party library structs

In addition to implementing it yourself, there are ready-made tools on Github, such as a third-party library: https://github.com/fatih/structs.

The custom structure tag it uses is structs:

1
2
3
4
5
// UserInfo
type UserInfo struct {
	Name string `json:"name" structs:"name"`
	Age  int    `json:"age" structs:"age"`
}

The usage is simple:

1
2
3
4
m3 := structs.Map(&u1)
for k, v := range m3 {
	fmt.Printf("key:%v value:%v value type:%T\n", k, v, v)
}

The structs package also has many other usage examples, so you can check the documentation. However, it should be noted that this library is currently set to read-only by the author.

nested struct to map[string]interface

structs itself supports nested structs to map[string]interface{}, and it will convert to map[string]interface{} nested in map[string]interface{} when it encounters a nested struct.

We define a set of nested structs as follows:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// 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"`
}

Declare the structure variable u1:

1
u1 := UserInfo{Name: "q1mi", Age: 18, Profile: Profile{"Two Color Ball"}}

Third-party library structs

The code is actually the same as above:

1
2
3
4
m3 := structs.Map(&u1)
for k, v := range m3 {
	fmt.Printf("key:%v value:%v value type:%T\n", k, v, v)
}

Output:

1
2
3
key:name value:q1mi value type:string
key:age value:18 value type:int
key:profile value:map[hobby:Two Color Ball] value type:map[string]interface {}

From the result, the last nested field profile is map[string]interface {}, which is a map nested map.

Use reflection to convert to single layer map

What if we want to convert a nested structure into a single level map?

Let’s take the above reflected code and make some simple changes:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
// ToMap2 Converting structures to single-level maps
func ToMap2(in interface{}, tag string) (map[string]interface{}, error) {

	// The current function only receives struct types
	v := reflect.ValueOf(in)
	if v.Kind() == reflect.Ptr { // Structure Pointer
		v = v.Elem()
	}
	if v.Kind() != reflect.Struct {
		return nil, fmt.Errorf("ToMap only accepts struct or struct pointer; got %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 { // Structure Pointer
			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 { // Embedded Pointer
				vi = vi.Elem()
				if vi.Kind() == reflect.Struct { // Structures
					queue = append(queue, vi.Interface())
				} else {
					ti := t.Field(i)
					if tagValue := ti.Tag.Get(tag); tagValue != "" {
						// Save to map
						out[tagValue] = vi.Interface()
					}
				}
				break
			}
			if vi.Kind() == reflect.Struct { // Embedded Structs
				queue = append(queue, vi.Interface())
				break
			}
			// General Fields
			ti := t.Field(i)
			if tagValue := ti.Tag.Get(tag); tagValue != "" {
				// Save to map
				out[tagValue] = vi.Interface()
			}
		}
	}
	return out, nil
}

To test:

1
2
3
4
m4, _ := ToMap2(&u1, "json")
for k, v := range m4 {
	fmt.Printf("key:%v value:%v value type:%T\n", k, v, v)
}

Output:

1
2
3
key:name value:q1mi value type:string
key:age value:18 value type:int
key:hobby value:Two Color Ball value type:string

This converts the nested structure to a single-level map, but note that the fields of the structure and the nested structure in this scenario will need to avoid duplication.


Reference https://www.liwenzhou.com/posts/Go/struct2map/