Convertir mapa en estructura

94

Estoy tratando de crear un método genérico en Go que llenará un structuso de datos de un map[string]interface{}. Por ejemplo, la firma y el uso del método podrían verse así:

func FillStruct(data map[string]interface{}, result interface{}) {
    ...
}

type MyStruct struct {
    Name string
    Age  int64
}

myData := make(map[string]interface{})
myData["Name"] = "Tony"
myData["Age"]  = 23

result := &MyStruct{}
FillStruct(myData, result)

// result now has Name set to "Tony" and Age set to 23

Sé que esto se puede hacer usando JSON como intermediario; ¿Existe otra forma más eficiente de hacer esto?

tgrosinger
fuente
1
El uso de JSON como intermediario usará la reflexión de todos modos ... asumiendo que va a usar el encoding/jsonpaquete stdlib para hacer ese paso intermedio ... ¿Puede dar un mapa de ejemplo y una estructura de ejemplo en la que este método podría usarse?
Simon Whitehead
Sí, esa es la razón por la que trato de evitar JSON. Parece que, con suerte, hay un método más eficiente que no conozco.
tgrosinger
¿Puede dar un ejemplo de caso de uso? Como en: ¿mostrar algún pseudocódigo que demuestre lo que hará este método?
Simon Whitehead
Mmm ... puede que haya una forma con el unsafepaquete ... pero no me atrevo a intentarlo. Aparte de eso ... Se requiere reflexión, ya que debe poder consultar los metadatos asociados con un tipo para colocar datos en sus propiedades. Sería bastante sencillo envolver esto en json.Marshal+ json.Decodellamadas ... pero eso es el doble de reflejo.
Simon Whitehead
He eliminado mi comentario sobre la reflexión. Estoy más interesado en hacer esto de la manera más eficiente posible. Si eso significa usar la reflexión, está bien.
tgrosinger

Respuestas:

110

La forma más sencilla sería utilizar https://github.com/mitchellh/mapstructure

import "github.com/mitchellh/mapstructure"

mapstructure.Decode(myData, &result)

Si quiere hacerlo usted mismo, puede hacer algo como esto:

http://play.golang.org/p/tN8mxT_V9h

func SetField(obj interface{}, name string, value interface{}) error {
    structValue := reflect.ValueOf(obj).Elem()
    structFieldValue := structValue.FieldByName(name)

    if !structFieldValue.IsValid() {
        return fmt.Errorf("No such field: %s in obj", name)
    }

    if !structFieldValue.CanSet() {
        return fmt.Errorf("Cannot set %s field value", name)
    }

    structFieldType := structFieldValue.Type()
    val := reflect.ValueOf(value)
    if structFieldType != val.Type() {
        return errors.New("Provided value type didn't match obj field type")
    }

    structFieldValue.Set(val)
    return nil
}

type MyStruct struct {
    Name string
    Age  int64
}

func (s *MyStruct) FillStruct(m map[string]interface{}) error {
    for k, v := range m {
        err := SetField(s, k, v)
        if err != nil {
            return err
        }
    }
    return nil
}

func main() {
    myData := make(map[string]interface{})
    myData["Name"] = "Tony"
    myData["Age"] = int64(23)

    result := &MyStruct{}
    err := result.FillStruct(myData)
    if err != nil {
        fmt.Println(err)
    }
    fmt.Println(result)
}
Dave
fuente
1
Gracias. Estoy usando una versión ligeramente modificada. play.golang.org/p/_JuMm6HMnU
tgrosinger
Quiero el comportamiento de FillStruct en todas mis diversas estructuras y no tengo que definirlas func (s MyStr...) FillStruct ...para todas. ¿Es posible definir FillStruct para una estructura base y luego hacer que todas mis otras estructuras 'hereden' ese comportamiento? En el paradigma anterior no es posible ya que solo la estructura base ... en este caso, "MyStruct" tendrá sus campos iterados
StartupGuy
Quiero decir, podría hacer que funcione para cualquier estructura con algo como esto: play.golang.org/p/0weG38IUA9
dave
¿Es posible implementar etiquetas en Mystruct?
vicTROLLA
1
@abhishek, ciertamente hay una penalización de rendimiento que pagará primero por ordenar el texto y luego por deshacerlo. Ese enfoque también es ciertamente más simple. Es una compensación y, en general, optaría por la solución más simple. Respondí con esta solución porque la pregunta decía "Sé que esto se puede hacer usando JSON como intermediario; ¿hay otra forma más eficiente de hacer esto?". Esta solución será más eficiente, la solución JSON generalmente será más fácil de implementar y razonar.
Dave
72

La biblioteca https://github.com/mitchellh/mapstructure de Hashicorp hace esto de inmediato :

import "github.com/mitchellh/mapstructure"

mapstructure.Decode(myData, &result)

El segundo resultparámetro tiene que ser una dirección de la estructura.

yunspace
fuente
¿Qué pasa si la clave del mapa es user_namey la estructura archivada es UserName?
Nicholas Jela
1
@NicholasJela puede manejar eso con etiquetas godoc.org/github.com/mitchellh/mapstructure#ex-Decode--Tags
Circuito en la pared
¿Qué pasa si map kye es _id y el nombre del mapa es Id, entonces no lo decodificará?
Ravi Shankar
26
  • la forma más sencilla de hacerlo es usando encoding/jsonpackage

solo por ejemplo:

package main
import (
    "fmt"
    "encoding/json"
)

type MyAddress struct {
    House string
    School string
}
type Student struct {
    Id int64
    Name string
    Scores float32
    Address MyAddress
    Labels []string
}

func Test() {

    dict := make(map[string]interface{})
    dict["id"] = 201902181425       // int
    dict["name"] = "jackytse"       // string
    dict["scores"] = 123.456        // float
    dict["address"] = map[string]string{"house":"my house", "school":"my school"}   // map
    dict["labels"] = []string{"aries", "warmhearted", "frank"}      // slice

    jsonbody, err := json.Marshal(dict)
    if err != nil {
        // do error check
        fmt.Println(err)
        return
    }

    student := Student{}
    if err := json.Unmarshal(jsonbody, &student); err != nil {
        // do error check
        fmt.Println(err)
        return
    }

    fmt.Printf("%#v\n", student)
}

func main() {
    Test()
}
jackytse
fuente
1
Gracias @jackytse. ¡Esta es realmente la mejor manera de hacerlo! La estructura del mapa no suele funcionar con un mapa anidado dentro de una interfaz. Por lo tanto, es mejor considerar una interfaz de cadena de mapa y manejarla como un json.
Gilles Essoki
Vaya al enlace del patio de recreo para el fragmento anterior: play.golang.org/p/JaKxETAbsnT
Junaid
13

Puede hacerlo ... puede ponerse un poco feo y se enfrentará a un poco de prueba y error en términos de tipos de mapeo ... pero aquí está la esencia básica:

func FillStruct(data map[string]interface{}, result interface{}) {
    t := reflect.ValueOf(result).Elem()
    for k, v := range data {
        val := t.FieldByName(k)
        val.Set(reflect.ValueOf(v))
    }
}

Muestra de trabajo: http://play.golang.org/p/PYHz63sbvL

Simon Whitehead
fuente
1
Esto parece entrar en pánico en los valores cero:reflect: call of reflect.Value.Set on zero Value
James Taylor
@JamesTaylor Sí. Mi respuesta asume que sabe exactamente qué campos está mapeando. Si busca una respuesta similar con más manejo de errores (incluido el error que está experimentando), sugeriría la respuesta de Daves.
Simon Whitehead
2

Adapto la respuesta de Dave y agrego una característica recursiva. Todavía estoy trabajando en una versión más fácil de usar. Por ejemplo, una cadena de números en el mapa debería poder convertirse a int en la estructura.

package main

import (
    "fmt"
    "reflect"
)

func SetField(obj interface{}, name string, value interface{}) error {

    structValue := reflect.ValueOf(obj).Elem()
    fieldVal := structValue.FieldByName(name)

    if !fieldVal.IsValid() {
        return fmt.Errorf("No such field: %s in obj", name)
    }

    if !fieldVal.CanSet() {
        return fmt.Errorf("Cannot set %s field value", name)
    }

    val := reflect.ValueOf(value)

    if fieldVal.Type() != val.Type() {

        if m,ok := value.(map[string]interface{}); ok {

            // if field value is struct
            if fieldVal.Kind() == reflect.Struct {
                return FillStruct(m, fieldVal.Addr().Interface())
            }

            // if field value is a pointer to struct
            if fieldVal.Kind()==reflect.Ptr && fieldVal.Type().Elem().Kind() == reflect.Struct {
                if fieldVal.IsNil() {
                    fieldVal.Set(reflect.New(fieldVal.Type().Elem()))
                }
                // fmt.Printf("recursive: %v %v\n", m,fieldVal.Interface())
                return FillStruct(m, fieldVal.Interface())
            }

        }

        return fmt.Errorf("Provided value type didn't match obj field type")
    }

    fieldVal.Set(val)
    return nil

}

func FillStruct(m map[string]interface{}, s interface{}) error {
    for k, v := range m {
        err := SetField(s, k, v)
        if err != nil {
            return err
        }
    }
    return nil
}

type OtherStruct struct {
    Name string
    Age  int64
}


type MyStruct struct {
    Name string
    Age  int64
    OtherStruct *OtherStruct
}



func main() {
    myData := make(map[string]interface{})
    myData["Name"]        = "Tony"
    myData["Age"]         = int64(23)
    OtherStruct := make(map[string]interface{})
    myData["OtherStruct"] = OtherStruct
    OtherStruct["Name"]   = "roxma"
    OtherStruct["Age"]    = int64(23)

    result := &MyStruct{}
    err := FillStruct(myData,result)
    fmt.Println(err)
    fmt.Printf("%v %v\n",result,result.OtherStruct)
}
rox
fuente
1

Hay dos pasos:

  1. Convertir la interfaz a JSON Byte
  2. Convierte JSON Byte en estructura

A continuación se muestra un ejemplo:

dbByte, _ := json.Marshal(dbContent)
_ = json.Unmarshal(dbByte, &MyStruct)
Nick L
fuente