Conversione di map in struct


94

Sto cercando di creare un metodo generico in Go che riempirà un structutilizzo di dati da un file map[string]interface{}. Ad esempio, la firma e l'utilizzo del metodo potrebbero essere simili 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

So che questo può essere fatto utilizzando JSON come intermediario; c'è un altro modo più efficiente per farlo?


1
Usare JSON come intermediario utilizzerà comunque la reflection .. supponendo che utilizzerai il encoding/jsonpacchetto stdlib per fare quel passaggio intermedio .. Puoi fornire una mappa di esempio e una struttura di esempio su cui questo metodo potrebbe essere utilizzato?
Simon Whitehead

Sì, questo è il motivo per cui sto cercando di evitare JSON. Sembra che ci sia, si spera, un metodo più efficiente di cui non sono a conoscenza.
tgrosinger

Puoi fornire un esempio di caso d'uso? Come in - mostra uno pseudocodice che dimostra cosa farà questo metodo?
Simon Whitehead

Mmm ... potrebbe esserci un modo con il unsafepacchetto .. ma non oso provarlo. Oltre a questo .. È necessaria la riflessione, poiché è necessario essere in grado di interrogare i metadati associati a un tipo per inserire i dati nelle sue proprietà. Sarebbe abbastanza semplice includerlo in json.Marshal+ json.Decodechiamate .. ma questo è il doppio del riflesso.
Simon Whitehead

Ho rimosso il mio commento sulla riflessione. Sono più interessato a farlo nel modo più efficiente possibile. Se questo significa usare la riflessione, va bene.
tgrosinger

Risposte:


110

Il modo più semplice sarebbe usare https://github.com/mitchellh/mapstructure

import "github.com/mitchellh/mapstructure"

mapstructure.Decode(myData, &result)

Se vuoi farlo da solo, potresti fare qualcosa del genere:

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

1
Grazie. Sto usando una versione leggermente modificata. play.golang.org/p/_JuMm6HMnU
tgrosinger

Voglio il comportamento FillStruct su tutti i miei vari struct e non devo definirlo func (s MyStr...) FillStruct ...per ognuno. È possibile definire FillStruct per uno struct di base e quindi fare in modo che tutti gli altri miei struct 'ereditino' quel comportamento? Nel paradigma sopra non è possibile poiché solo la struttura di base ... in questo caso "MyStruct" avrà effettivamente i suoi campi iterati
StartupGuy

Voglio dire che potresti farlo funzionare per qualsiasi struttura con qualcosa del genere: play.golang.org/p/0weG38IUA9
dave

È possibile implementare i tag in Mystruct?
vicTROLLA

1
@abhishek c'è sicuramente una penalità per le prestazioni che pagherai per il primo marshalling in testo e poi unmarshaling. Anche questo approccio è certamente più semplice. È un compromesso e generalmente sceglierei la soluzione più semplice. Ho risposto con questa soluzione perché la domanda diceva "So che questo può essere fatto utilizzando JSON come intermediario; esiste un altro modo più efficiente per farlo?". Questa soluzione sarà più efficiente, la soluzione JSON sarà generalmente più facile da implementare e ragionare.
dave

72

La libreria https://github.com/mitchellh/mapstructure di Hashicorp lo fa fuori dagli schemi:

import "github.com/mitchellh/mapstructure"

mapstructure.Decode(myData, &result)

Il secondo resultparametro deve essere un indirizzo della struttura.


cosa succede se la chiave della mappa è user_namee la struttura archiviata è UserName?
Nicholas Jela


cosa succede se map kye è _id e il nome della mappa è Id, allora non lo decodifica.
Ravi Shankar,

25
  • il modo più semplice per farlo è usare encoding/jsonpackage

solo per esempio:

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

1
Grazie @jackytse. Questo è effettivamente il modo migliore per farlo !! la struttura della mappa spesso non funziona con la mappa nidificata all'interno di un'interfaccia. Quindi è meglio considerare un'interfaccia di stringa di mappa e gestirla come un json.
Gilles Essoki

Vai al link del parco giochi per lo snippet sopra: play.golang.org/p/JaKxETAbsnT
Junaid

13

Puoi farlo ... potrebbe diventare un po 'brutto e dovrai affrontare alcuni tentativi ed errori in termini di tipi di mappatura .. ma ecco l'essenza di base:

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

Esempio di lavoro: http://play.golang.org/p/PYHz63sbvL


1
Questo sembra andare nel panico sui valori zero:reflect: call of reflect.Value.Set on zero Value
James Taylor,

@JamesTaylor Sì. La mia risposta presuppone che tu sappia esattamente quali campi stai mappando. Se stai cercando una risposta simile con una maggiore gestione degli errori (incluso l'errore che stai riscontrando), suggerirei invece di rispondere Daves.
Simon Whitehead

2

Adattamento la risposta di Dave e aggiungo una funzionalità ricorsiva. Sto ancora lavorando a una versione più user friendly. Ad esempio, una stringa numerica nella mappa dovrebbe essere in grado di essere convertita in int nella struttura.

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

1

Ci sono due passaggi:

  1. Converti l'interfaccia in JSON Byte
  2. Converti byte JSON in struct

Di seguito è riportato un esempio:

dbByte, _ := json.Marshal(dbContent)
_ = json.Unmarshal(dbByte, &MyStruct)
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.