Conversion de la carte en structure

94

J'essaie de créer une méthode générique dans Go qui remplira un en structutilisant les données d'un fichier map[string]interface{}. Par exemple, la signature et l'utilisation de la méthode peuvent ressembler à ceci:

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

Je sais que cela peut être fait en utilisant JSON comme intermédiaire; y a-t-il une autre façon plus efficace de faire cela?

tgrosinger
la source
1
L'utilisation de JSON comme intermédiaire utilisera de toute façon la réflexion ... en supposant que vous allez utiliser le encoding/jsonpackage stdlib pour faire cette étape intermédiaire. Pouvez-vous donner un exemple de carte et un exemple de structure sur lesquels cette méthode pourrait être utilisée?
Simon Whitehead
Oui, c'est la raison pour laquelle j'essaie d'éviter JSON. On dirait qu'il y a, espérons-le, une méthode plus efficace que je ne connais pas.
tgrosinger
Pouvez-vous donner un exemple d'utilisation? Comme dans - montrer un pseudocode qui montre ce que cette méthode fera?
Simon Whitehead
Mmm ... il y a peut-être un moyen avec le unsafepackage .. mais je n'ose pas l'essayer. Autre que cela .. Une réflexion est requise, car vous devez être en mesure d'interroger les métadonnées associées à un type afin de placer des données dans ses propriétés. Ce serait assez simple d'envelopper cela dans json.Marshal+ json.Decodecalls .. mais c'est le double du reflet.
Simon Whitehead
J'ai supprimé mon commentaire sur la réflexion. Je suis plus intéressé à le faire aussi efficacement que possible. Si cela signifie utiliser la réflexion, ce n'est pas grave.
tgrosinger

Réponses:

110

Le moyen le plus simple serait d'utiliser https://github.com/mitchellh/mapstructure

import "github.com/mitchellh/mapstructure"

mapstructure.Decode(myData, &result)

Si vous voulez le faire vous-même, vous pouvez faire quelque chose comme ceci:

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
la source
1
Je vous remercie. J'utilise une version légèrement modifiée. play.golang.org/p/_JuMm6HMnU
tgrosinger
Je veux le comportement FillStruct sur toutes mes différentes structures et je n'ai pas à définir func (s MyStr...) FillStruct ...pour chacun. Est-il possible de définir FillStruct pour une structure de base, puis toutes mes autres structures `` héritent '' de ce comportement? Dans le paradigme ci-dessus, ce n'est pas possible car seule la structure de base ... dans ce cas "MyStruct" aura en fait ses champs itérés
StartupGuy
Je veux dire que vous pourriez le faire fonctionner pour n'importe quelle structure avec quelque chose comme ceci: play.golang.org/p/0weG38IUA9
dave
Est-il possible d'implémenter des balises dans Mystruct?
vicTROLLA
1
@abhishek, il y a certainement une pénalité de performance que vous paierez pour le premier marshaling au texte, puis le démarshaling. Cette approche est également certainement plus simple. C'est un compromis, et généralement je choisirais la solution la plus simple. J'ai répondu avec cette solution parce que la question disait "Je sais que cela peut être fait en utilisant JSON comme intermédiaire; y a-t-il une autre façon plus efficace de faire cela?". Cette solution sera plus efficace, la solution JSON sera généralement plus simple à mettre en œuvre et à raisonner.
dave le
72

La bibliothèque https://github.com/mitchellh/mapstructure de Hashicorp le fait hors de la boîte:

import "github.com/mitchellh/mapstructure"

mapstructure.Decode(myData, &result)

Le deuxième resultparamètre doit être une adresse de la structure.

yunspace
la source
que se passe-t-il si la clé de la carte est user_nameet la structure classée est UserName?
Nicholas Jela
1
@NicholasJela il peut gérer cela avec les balises godoc.org/github.com/mitchellh/mapstructure#ex-Decode--Tags
Circuit in the wall
que se passe-t-il si la carte kye est _id et le nom de la carte est Id alors il ne la décodera pas.
Ravi Shankar
26
  • le moyen le plus simple de le faire est d'utiliser encoding/jsonpackage

juste par exemple:

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
la source
1
Merci @jackytse. C'est en fait la meilleure façon de le faire !! la structure de la carte ne fonctionne pas souvent avec une carte imbriquée dans une interface. Il est donc préférable de considérer une interface de chaîne de carte et de la gérer comme un json.
Gilles Essoki
Aller au lien de l'aire de jeux pour l'extrait ci-dessus: play.golang.org/p/JaKxETAbsnT
Junaid
13

Vous pouvez le faire ... cela peut devenir un peu moche et vous serez confronté à des essais et des erreurs en termes de types de mappage ... mais voici l'essentiel:

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

Exemple de travail: http://play.golang.org/p/PYHz63sbvL

Simon Whitehead
la source
1
Cela semble paniquer sur les valeurs nulles:reflect: call of reflect.Value.Set on zero Value
James Taylor
@JamesTaylor Oui. Ma réponse suppose que vous savez exactement quels champs vous mappez. Si vous recherchez une réponse similaire avec plus de gestion des erreurs (y compris l'erreur que vous rencontrez), je suggérerais plutôt une réponse à Daves.
Simon Whitehead
2

J'adapte la réponse de Dave et ajoute une fonction récursive. Je travaille toujours sur une version plus conviviale. Par exemple, une chaîne numérique dans la carte doit pouvoir être convertie en int dans la structure.

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
la source
1

Il y a deux étapes:

  1. Convertir l'interface en octet JSON
  2. Convertir l'octet JSON en struct

Voici un exemple:

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