Mengonversi peta menjadi struct

95

Saya mencoba membuat metode umum di Go yang akan mengisi structmenggunakan data dari a map[string]interface{}. Misalnya, tanda tangan metode dan penggunaannya mungkin terlihat seperti:

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

Saya tahu ini bisa dilakukan dengan menggunakan JSON sebagai perantara; apakah ada cara lain yang lebih efisien untuk melakukan ini?

tgrosinger.dll
sumber
1
Menggunakan JSON sebagai perantara akan tetap menggunakan refleksi .. dengan asumsi Anda akan menggunakan encoding/jsonpaket stdlib untuk melakukan langkah menengah itu .. Dapatkah Anda memberikan contoh peta dan contoh struct dimana metode ini dapat digunakan?
Simon Whitehead
Ya, itulah alasan saya mencoba menghindari JSON. Sepertinya ada metode yang lebih efisien yang saya tidak tahu.
tgrosinger
Bisakah Anda memberikan contoh use case? Seperti di - tampilkan beberapa pseudocode yang menunjukkan apa yang akan dilakukan metode ini?
Simon Whitehead
Mmm ... mungkin ada cara dengan unsafepaketnya .. tapi aku tidak berani mencobanya. Selain itu .. Diperlukan refleksi, karena Anda harus dapat mengkueri metadata yang terkait dengan suatu tipe untuk menempatkan data ke propertinya. Ini akan sangat mudah untuk membungkus ini dalam json.Marshal+ json.Decodepanggilan .. tapi itu menggandakan refleksi.
Simon Whitehead
Saya telah menghapus komentar saya tentang refleksi. Saya lebih tertarik melakukan ini seefisien mungkin. Jika itu berarti menggunakan refleksi tidak apa-apa.
tgrosinger

Jawaban:

114

Cara termudah adalah menggunakan https://github.com/mitchellh/mapstructure

import "github.com/mitchellh/mapstructure"

mapstructure.Decode(myData, &result)

Jika Anda ingin melakukannya sendiri, Anda dapat melakukan sesuatu seperti ini:

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
sumber
1
Terima kasih. Saya menggunakan versi yang sedikit dimodifikasi. play.golang.org/p/_JuMm6HMnU
tgrosinger
Saya ingin perilaku FillStruct di semua berbagai struct saya dan tidak harus menentukan func (s MyStr...) FillStruct ...untuk setiap orang. Apakah mungkin untuk mendefinisikan FillStruct untuk sebuah struktur dasar kemudian memiliki semua struct saya yang lain 'mewarisi' perilaku itu? Dalam paradigma di atas itu tidak mungkin karena hanya struktur dasar ... dalam hal ini "MyStruct" akan benar-benar memiliki bidangnya
diulang
Maksud saya, Anda dapat membuatnya berfungsi untuk struct apa pun dengan sesuatu seperti ini: play.golang.org/p/0weG38IUA9
dave
Apakah mungkin untuk menerapkan tag di Mystruct?
vicTROLLA
1
@abhishek pasti ada hukuman kinerja yang akan Anda bayarkan untuk marshaling pertama ke teks dan kemudian unmarshaling. Pendekatan itu juga tentunya lebih sederhana. Ini tradeoff, dan biasanya saya akan memilih solusi yang lebih sederhana. Saya menjawab dengan solusi ini karena pertanyaannya menyatakan "Saya tahu ini dapat dilakukan dengan menggunakan JSON sebagai perantara; apakah ada cara lain yang lebih efisien untuk melakukan ini?". Solusi ini akan lebih efisien, solusi JSON umumnya akan lebih mudah diimplementasikan dan dipertimbangkan.
Dave
74

Https://github.com/mitchellh/mapstructure library Hashicorp melakukan ini di luar kotak:

import "github.com/mitchellh/mapstructure"

mapstructure.Decode(myData, &result)

resultParameter kedua harus berupa alamat struct.

yunspace
sumber
bagaimana jika kunci peta adalah user_namedan struct diajukan UserName?
Nicholas Jela
1
@NicholasJela dapat mengatasinya dengan tag godoc.org/github.com/mitchellh/mapstructure#ex-Decode--Tags
Sirkuit di dinding
bagaimana jika map kye adalah _id dan nama peta adalah Id maka itu tidak akan memecahkan kodenya.
Ravi Shankar
27
  • cara termudah untuk melakukannya adalah dengan menggunakan encoding/jsonpaket

hanya sebagai contoh:

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
sumber
1
Terima kasih @jackytse. Ini sebenarnya cara terbaik untuk melakukan itu !! struktur peta sering tidak berfungsi dengan peta bersarang dalam sebuah antarmuka. Jadi lebih baik pertimbangkan antarmuka string peta dan tangani sebagai json.
Gilles Essoki
Buka tautan taman bermain untuk cuplikan di atas: play.golang.org/p/JaKxETAbsnT
Junaid
13

Anda bisa melakukannya ... mungkin akan sedikit jelek dan Anda akan dihadapkan pada beberapa trial and error dalam hal jenis pemetaan .. tapi inilah inti dasarnya:

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

Contoh kerja: http://play.golang.org/p/PYHz63sbvL

Simon Whitehead
sumber
1
Ini tampaknya panik pada nilai nol:reflect: call of reflect.Value.Set on zero Value
James Taylor
@JamesTlor Ya. Jawaban saya mengasumsikan Anda tahu persis bidang apa yang Anda petakan. Jika Anda mencari jawaban serupa dengan lebih banyak penanganan kesalahan (termasuk kesalahan yang Anda alami), saya akan menyarankan jawaban Daves sebagai gantinya.
Simon Whitehead
2

Saya mengadaptasi jawaban Dave, dan menambahkan fitur rekursif. Saya masih mengerjakan versi yang lebih ramah pengguna. Misalnya, string angka di peta harus bisa diubah menjadi int di struct.

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

Ada dua langkah:

  1. Ubah antarmuka ke JSON Byte
  2. Ubah JSON Byte menjadi struct

Berikut ini contohnya:

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