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?


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

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.


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

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


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

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)
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.