将结构体转换为Map的Golang函数

102
我想要在Golang中将一个结构体转换为map。如果可以使用JSON标签作为创建的map的键(否则默认使用字段名)会更好。
编辑于2020年12月14日: 由于structs存储库已存档,您可以改用mapstructure
编辑TL;DR版本,于2015年6月15日: 如果您想快速解决将结构转换为map的问题,请查看accepted answer,给它点赞并使用该软件包。
祝编码愉快! :)

原始帖子

到目前为止,我有这个函数,我正在使用反射包,但我不太理解如何使用该包,请耐心等待。

func ConvertToMap(model interface{}) bson.M {
    ret := bson.M{}

    modelReflect := reflect.ValueOf(model)

    if modelReflect.Kind() == reflect.Ptr {
        modelReflect = modelReflect.Elem()
    }

    modelRefType := modelReflect.Type()
    fieldsCount := modelReflect.NumField()

    var fieldData interface{}

    for i := 0; i < fieldsCount; i++ {
        field := modelReflect.Field(i)

        switch field.Kind() {
        case reflect.Struct:
            fallthrough
        case reflect.Ptr:
            fieldData = ConvertToMap(field.Interface())
        default:
            fieldData = field.Interface()
        }

        ret[modelRefType.Field(i).Name] = fieldData
    }

    return ret
}

我查看了JSON包的源代码,因为它应该包含我需要的实现(或部分内容),但理解有限。


1
你在这里尝试实现特定的目标吗?如果你正在处理mgo/bson包(由于使用了bson.M),它不是已经可以执行类似于encoding/json的结构体转换了吗? - James Henstridge
2
它涉及到反射、包reflect,而且使用起来又慢又麻烦;json包之所以这样,是因为使用反射很难。我的建议是要么使用已经为您完成反射部分的东西(像gorpmgo这样的对象到数据库接口,内置包像json),要么使用(可能重复的)手写代码来完全避免反射。这是一种情况,在这种情况下,在JavaScript中自然高效的方法在Go中并不适用。 - twotwotwo
4
https://github.com/mitchellh/mapstructure - 不过听听詹姆斯的意见。在这种情况下,没有理由尝试去做这件事情。 - elithrar
3
"mapstructure是一个用于将通用映射值解码为结构体以及反之的Go库。"(加粗部分为强调) - elithrar
1
@elithrar 是的,我看到了。很抱歉我在那个库上查看了好几次,但没有看到反之亦然。我在godoc上看了一下,但是没看到如何将结构体转换为映射表。不管怎样,谢谢。 - eAbi
显示剩余4条评论
7个回答

169

我也有类似这种需求。我使用了一个内部包,用于将结构体转换为 map。我决定将它开源,并加入其他基于struct的高级函数。请看这里:

https://github.com/fatih/structs

它支持以下功能:

  • 将结构体转换为 map
  • 提取结构体的字段到[]string
  • 提取结构体的值到[]values
  • 检查结构体是否已初始化
  • 检查传递的接口是结构体还是指向结构体的指针

您可以在这里查看一些示例:http://godoc.org/github.com/fatih/structs#pkg-examples 例如,将结构体转换为 map 是一个简单的操作:

type Server struct {
    Name    string
    ID      int32
    Enabled bool
}

s := &Server{
    Name:    "gopher",
    ID:      123456,
    Enabled: true,
}

// => {"Name":"gopher", "ID":123456, "Enabled":true}
m := structs.Map(s)

structs 包支持匿名(嵌入式)字段和嵌套结构体。该包提供了通过字段标签过滤某些字段的功能。


6
哇,@Fatih Arslan,这太棒了。这实际上是将结构体“编组”成与encoding/json.Marshal()类似的map[string]interface{},使用结构标签等。太神奇了! - Ory Band
3
这里有一种使用“json”标签名称而不是“structs”标签名称的方法。 https://github.com/fatih/structs/issues/25 - MRHwick
44
请注意,这个项目现已存档,不再进行维护。 - rfay
2
我不想在我的项目中添加其他依赖。 - SouvikMaji
1
不要忘记在你的结构体上导出字段(犯了新手错误)。 - Guy
显示剩余6条评论

89

structmap[string]interface{}

package main

import (
    "fmt"
    "encoding/json"
)

type MyData struct {
    One   int
    Two   string
    Three int
}

func main() {   
    in := &MyData{One: 1, Two: "second"}

    var inInterface map[string]interface{}
    inrec, _ := json.Marshal(in)
    json.Unmarshal(inrec, &inInterface)

    // iterate through inrecs
    for field, val := range inInterface {
            fmt.Println("KV Pair: ", field, val)
    }
}

这里是 Go Playground


5
为了让代码更简单些,不要使用 var inInterface interface{},而是改用 var inInterface map[string]interface{}。这里提供一个 playground 链接供参考:https://play.golang.org/p/woUiMzL_1X - alextanhongpin
5
看起来像是黑客攻击,我找不到任何干净或简单的东西。 - John White
3
这种方法存在问题。在生成的地图中,整数值被转换为float64类型,这是意料之外的行为。因此,inInterface["One"]的值将会是float64类型而不是int类型。 - Bruno Negrão Zica
6
编组再解组似乎是一种非常低效的转换数据的方式。 - d4nyll
2
@d4nyll,你有其他的解决方案吗?当然不能直接使用反射。 - Fred Hors
显示剩余2条评论

25

以下是我之前编写的函数,用于将结构体转换为映射表,使用标签作为键

// ToMap converts a struct to a map using the struct's tags.
//
// ToMap uses tags on struct fields to decide which fields to add to the
// returned map.
func ToMap(in interface{}, tag string) (map[string]interface{}, error){
    out := make(map[string]interface{})

    v := reflect.ValueOf(in)
    if v.Kind() == reflect.Ptr {
        v = v.Elem()
    }

    // we only accept structs
    if v.Kind() != reflect.Struct {
        return nil, fmt.Errorf("ToMap only accepts structs; got %T", v)
    }

    typ := v.Type()
    for i := 0; i < v.NumField(); i++ {
        // gets us a StructField
        fi := typ.Field(i)
        if tagv := fi.Tag.Get(tag); tagv != "" {
            // set key of map to value in struct field
            out[tagv] = v.Field(i).Interface()
        }
    }
    return out, nil
}

在这里查看可运行的示例

请注意,如果您有多个带有相同标记值的字段,则显然无法将它们全部存储在映射中。如果发生这种情况,最好返回一个错误。


嗯,但我认为它不能处理嵌套结构体,对吧?我认为必须递归遍历结构体字段。 - eAbi
你的问题没有提到这一点。所以你想要一个结构体展开成一个映射吗? - Edwardr
1
是的,一个结构体可以展开成一个映射(因此子结构将变成子映射)。如果字段包含结构,则可以通过再次调用函数来实现此目的。 - eAbi

14

我喜欢被采纳的答案中提到的可导入包,但它不能翻译我的JSON别名。我的大部分项目都有一个帮助函数/类,我会导入它。

这是一个解决我的特定问题的函数。


// Converts a struct to a map while maintaining the json alias as keys
func StructToMap(obj interface{}) (newMap map[string]interface{}, err error) {
    data, err := json.Marshal(obj) // Convert to a json string

    if err != nil {
        return
    }

    err = json.Unmarshal(data, &newMap) // Convert to a map
    return
}

总的来说,这就是它被称为的方式...

package main

import (
    "fmt"
    "encoding/json"
    "github.com/fatih/structs"
)

type MyStructObject struct {
    Email string `json:"email_address"`
}

func main() {
    obj := &MyStructObject{Email: "test@test.com"}

    // My solution
    fmt.Println(StructToMap(obj)) // prints {"email_address": "test@test.com"}

    // The currently accepted solution
    fmt.Println(structs.Map(obj)) // prints {"Email": "test@test.com"}
}

2
它能工作,但不是最佳解决方案,会执行双重编码,在性能关键时不应使用。比手动创建映射并添加键/值慢大约10倍。 - Tigran Babajanyan

4
package main

import (
    "fmt"
    "reflect"
)

type bill struct {
    N1 int
    N2 string
    n3 string
}

func main() {
    a := bill{4, "dhfthf", "fdgdf"}

    v := reflect.ValueOf(a)

    values := make(map[string]interface{}, v.NumField())

    for i := 0; i < v.NumField(); i++ {
        if v.Field(i).CanInterface() {
            values[v.Type().Field(i).Name] = v.Field(i).Interface()
        } else {
            fmt.Printf("sorry you have a unexported field (lower case) value you are trying to sneak past. I will not allow it: %v\n", v.Type().Field(i).Name)
        }
    }

    fmt.Println(values)

    passObject(&values)
}

func passObject(v1 *map[string]interface{}) {
    fmt.Println("yoyo")
}

这也适用于来自不同包的结构体。(注意,您无法访问未公开的字段) - derek

1

我有点晚了,但我需要这种功能,所以我写了这个。可以解决嵌套结构。默认使用字段名,但也可以使用自定义标签。一个副作用是,如果你将tagTitle常量设置为json,你就可以使用你已经有的json标签。

package main

import (
    "fmt"
    "reflect"
)

func StructToMap(val interface{}) map[string]interface{} {
    //The name of the tag you will use for fields of struct
    const tagTitle = "kelvin"

    var data map[string]interface{} = make(map[string]interface{})
    varType := reflect.TypeOf(val)
    if varType.Kind() != reflect.Struct {
        // Provided value is not an interface, do what you will with that here
        fmt.Println("Not a struct")
        return nil
    }

    value := reflect.ValueOf(val)
    for i := 0; i < varType.NumField(); i++ {
        if !value.Field(i).CanInterface() {
            //Skip unexported fields
            continue
        }
        tag, ok := varType.Field(i).Tag.Lookup(tagTitle)
        var fieldName string
        if ok && len(tag) > 0 {
            fieldName = tag
        } else {
            fieldName = varType.Field(i).Name
        }
        if varType.Field(i).Type.Kind() != reflect.Struct {
            data[fieldName] = value.Field(i).Interface()
        } else {
            data[fieldName] = StructToMap(value.Field(i).Interface())
        }

    }

    return data
}


你的回答可以通过提供更多支持信息来改进。请编辑以添加进一步的细节,例如引用或文档,以便他人可以确认你的答案是正确的。您可以在帮助中心中找到有关如何编写良好答案的更多信息。 - Community

-5
map := Structpb.AsMap()

// map is the map[string]interface{}

2
这与被接受的答案有何不同? - FloLie
@FloLie 这是一个预定义的库,它将减少你的工作量并返回你所期望的响应。 - Aman Agarwal
复制粘贴代码并不是有帮助的。下次请留下代码的解释,包括它的作用、来源以及为什么选择这种方法而不是其他方法。 - Carlo Nyte

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接