Konwertowanie map do struct

95

Próbuję utworzyć ogólną metodę w Go, która wypełni structdane using z pliku map[string]interface{}. Na przykład podpis metody i użycie może wyglądać następująco:

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

Wiem, że można to zrobić używając JSON jako pośrednika; czy jest inny skuteczniejszy sposób na zrobienie tego?

tgrosinger
źródło
1
Używanie JSON jako pośrednika i tak spowoduje użycie odbicia ... zakładając, że będziesz używać encoding/jsonpakietu stdlib do wykonania tego kroku pośredniego. Czy możesz podać przykładową mapę i przykładową strukturę, na której ta metoda może być używana?
Simon Whitehead
Tak, to jest powód, dla którego staram się unikać JSON. Wygląda na to, że mam nadzieję, że jest bardziej skuteczna metoda, o której nie wiem.
tgrosinger
Czy możesz podać przykład użycia? Jak w - pokaż jakiś pseudokod, który demonstruje, co zrobi ta metoda?
Simon Whitehead
Mmm… może być sposób z unsafepaczką… ale nie śmiem tego spróbować. Poza tym .. Refleksja jest wymagana, ponieważ musisz mieć możliwość odpytywania metadanych skojarzonych z typem, aby umieścić dane we właściwościach. Byłoby dość proste zawinięcie tego w json.Marshal+ json.Decodewywołania ... ale to jest podwójna refleksja.
Simon Whitehead
Usunąłem swój komentarz dotyczący refleksji. Bardziej interesuje mnie robienie tego tak wydajnie, jak to tylko możliwe. Jeśli to oznacza używanie refleksji, to jest w porządku.
tgrosinger

Odpowiedzi:

117

Najprostszym sposobem byłoby użycie https://github.com/mitchellh/mapstructure

import "github.com/mitchellh/mapstructure"

mapstructure.Decode(myData, &result)

Jeśli chcesz to zrobić sam, możesz zrobić coś takiego:

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
źródło
2
Dziękuję Ci. Używam nieco zmodyfikowanej wersji. play.golang.org/p/_JuMm6HMnU
tgrosinger
Chcę, aby zachowanie FillStruct we wszystkich moich różnych strukturach i nie musiało definiować func (s MyStr...) FillStruct ...dla każdej z nich. Czy można zdefiniować FillStruct dla struktury bazowej, a następnie mieć wszystkie inne struktury „dziedziczą” to zachowanie? W powyższym paradygmacie nie jest to możliwe, ponieważ tylko struktura bazowa ... w tym przypadku "MyStruct" będzie faktycznie miał iterowane pola
StartupGuy
Chodzi mi o to, że możesz sprawić
dave
Czy można zaimplementować tagi w Mystruct?
vicTROLLA
1
@abhishek z pewnością jest kara za wydajność, którą zapłacisz za pierwsze skierowanie do tekstu, a następnie cofnięcie. Takie podejście jest z pewnością prostsze. To kompromis i ogólnie wybrałbym prostsze rozwiązanie. Odpowiedziałem na to rozwiązanie, ponieważ pytanie brzmiało: „Wiem, że można to zrobić używając JSON jako pośrednika; czy jest inny bardziej efektywny sposób na zrobienie tego?”. To rozwiązanie będzie bardziej wydajne, rozwiązanie JSON będzie generalnie łatwiejsze do wdrożenia i uzasadnienia.
dave
74

Biblioteka Hashicorp https://github.com/mitchellh/mapstructure robi to po wyjęciu z pudełka:

import "github.com/mitchellh/mapstructure"

mapstructure.Decode(myData, &result)

Drugi resultparametr musi być adresem struktury.

yunspace
źródło
co jeśli klucz mapy jest, user_namea pole struct to UserName?
Nicholas Jela
1
@NicholasJela poradzi sobie z tym za pomocą tagów godoc.org/github.com/mitchellh/mapstructure#ex-Decode--Tags
Obwód w ścianie
co jeśli mapa kye to _id, a nazwa mapy to Id, to nie zdekoduje jej.
Ravi Shankar
27
  • najprostszym sposobem jest użycie encoding/jsonpakietu

tylko na przykład:

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
źródło
1
Dzięki @jackytse. To właściwie najlepszy sposób na zrobienie tego !! struktura mapy często nie działa z mapami zagnieżdżonymi w interfejsie. Więc lepiej rozważyć interfejs łańcucha mapy i traktować go jako json.
Gilles Essoki
Przejdź do linku do placu zabaw dla powyższego fragmentu: play.golang.org/p/JaKxETAbsnT
Junaid
13

Możesz to zrobić ... może być trochę brzydko i będziesz musiał zmierzyć się z próbami i błędami w zakresie typów mapowania ... ale oto podstawowa istota tego:

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

Próbka robocza: http://play.golang.org/p/PYHz63sbvL

Simon Whitehead
źródło
1
To wydaje się panikować przy zerowych wartościach:reflect: call of reflect.Value.Set on zero Value
James Taylor
@JamesTaylor Yes. Moja odpowiedź zakłada, że ​​wiesz dokładnie, jakie pola mapujesz. Jeśli szukasz podobnej odpowiedzi z większą obsługą błędów (w tym napotkanego błędu), sugerowałbym zamiast tego odpowiedź Davesa.
Simon Whitehead,
2

Dostosowuję odpowiedź Dave'a i dodaję funkcję rekurencyjną. Nadal pracuję nad bardziej przyjazną dla użytkownika wersją. Na przykład ciąg liczbowy w mapie powinien być w stanie przekonwertować na int w strukturze.

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
źródło
1

Istnieją dwa kroki:

  1. Konwertuj interfejs na bajt JSON
  2. Konwertuj bajt JSON na struct

Poniżej przykład:

dbByte, _ := json.Marshal(dbContent)
_ = json.Unmarshal(dbByte, &MyStruct)
Nick L.
źródło