Convertendo mapa em estrutura

94

Estou tentando criar um método genérico em Go que preencherá um structusando dados de a map[string]interface{}. Por exemplo, a assinatura e o uso do método podem ser semelhantes a:

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

Eu sei que isso pode ser feito usando JSON como um intermediário; existe outra maneira mais eficiente de fazer isso?

Tgrosinger
fonte
1
Usar JSON como intermediário usará reflexão de qualquer maneira .. presumindo que você usará o encoding/jsonpacote stdlib para fazer essa etapa intermediária .. Você pode fornecer um mapa de exemplo e uma estrutura de exemplo em que este método pode ser usado?
Simon Whitehead
Sim, é por isso que estou tentando evitar JSON. Parece que há um método mais eficiente que eu não conheço.
tgrosinger
Você pode dar um exemplo de caso de uso? Como em - mostrar algum pseudocódigo que demonstra o que esse método fará?
Simon Whitehead
Mmm ... pode haver uma maneira com o unsafepacote .. mas não me atrevo a tentar. Fora isso .. A reflexão é necessária, pois você precisa ser capaz de consultar os metadados associados a um tipo para colocar os dados em suas propriedades. Seria bastante simples envolver isso em chamadas json.Marshal+ json.Decode.. mas isso é o dobro do reflexo.
Simon Whitehead
Eu removi meu comentário sobre reflexão. Estou mais interessado em fazer isso da forma mais eficiente possível. Se isso significa usar reflexão, tudo bem.
tgrosinger

Respostas:

110

A maneira mais simples seria usar https://github.com/mitchellh/mapstructure

import "github.com/mitchellh/mapstructure"

mapstructure.Decode(myData, &result)

Se você quiser fazer isso sozinho, pode fazer algo assim:

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
fonte
1
Obrigado. Estou usando uma versão ligeiramente modificada. play.golang.org/p/_JuMm6HMnU
tgrosinger
Eu quero o comportamento FillStruct em todas as minhas várias estruturas e não tenho que definir func (s MyStr...) FillStruct ...para cada uma. É possível definir FillStruct para uma estrutura de base e fazer com que todas as minhas outras estruturas 'herdem' esse comportamento? No paradigma acima não é possível, pois apenas a estrutura de base ... neste caso "MyStruct" realmente terá seus campos iterados
StartupGuy
Quero dizer, você poderia fazer com que funcionasse para qualquer estrutura com algo assim: play.golang.org/p/0weG38IUA9
dave
É possível implementar tags em Mystruct?
vicTROLLA
1
@abhishek certamente há uma penalidade de desempenho que você pagará primeiro pelo empacotamento para texto e depois pelo desempacotamento. Essa abordagem também é certamente mais simples. É uma troca e geralmente optaria pela solução mais simples. Respondi com esta solução porque a pergunta afirmava "Sei que isso pode ser feito usando JSON como um intermediário; existe outra maneira mais eficiente de fazer isso?". Essa solução será mais eficiente, a solução JSON geralmente será mais fácil de implementar e raciocinar.
Dave
72

A biblioteca https://github.com/mitchellh/mapstructure da Hashicorp faz isso fora da caixa:

import "github.com/mitchellh/mapstructure"

mapstructure.Decode(myData, &result)

O segundo resultparâmetro deve ser um endereço da estrutura.

espaço aéreo
fonte
e se a chave do mapa for user_namee o campo de estrutura for UserName?
Nicholas Jela
1
@NicholasJela pode lidar com isso com tags godoc.org/github.com/mitchellh/mapstructure#ex-Decode--Tags
Circuito na parede
e se map kye for _id e o nome do mapa for Id, então ele não o decodificará.
Ravi Shankar de
25
  • a maneira mais simples de fazer isso é usando o encoding/jsonpacote

apenas por exemplo:

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
fonte
1
Obrigado @jackytse. Esta é realmente a melhor maneira de fazer isso !! a estrutura do mapa geralmente não funciona com o mapa aninhado em uma interface. Portanto, é melhor considerar uma interface de string de mapa e tratá-la como um json.
Gilles Essoki
Go parque ligação para o trecho acima: play.golang.org/p/JaKxETAbsnT
Junaid
13

Você pode fazer isso ... pode ficar um pouco feio e você se deparará com algumas tentativas e erros em termos de tipos de mapeamento ... mas aqui está a essência disso:

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))
    }
}

Amostra de trabalho: http://play.golang.org/p/PYHz63sbvL

Simon Whitehead
fonte
1
Isso parece causar pânico em valores zero:reflect: call of reflect.Value.Set on zero Value
James Taylor
@JamesTaylor Sim. Minha resposta assume que você sabe exatamente quais campos está mapeando. Se você deseja uma resposta semelhante com mais tratamento de erros (incluindo o erro que está ocorrendo), sugiro a resposta de Daves.
Simon Whitehead
2

Eu adapto a resposta de Dave e adiciono um recurso recursivo. Ainda estou trabalhando em uma versão mais amigável. Por exemplo, uma string numérica no mapa deve ser capaz de ser convertida em int na estrutura.

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
fonte
1

Existem duas etapas:

  1. Converter interface para JSON Byte
  2. Converter Byte JSON em estrutura

Abaixo está um exemplo:

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