Преобразование карты в структуру

95

Я пытаюсь создать в Go общий метод, который будет заполнять structданные из файла map[string]interface{}. Например, подпись и использование метода могут выглядеть так:

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

Я знаю, что это можно сделать, используя JSON в качестве посредника; есть ли другой более эффективный способ сделать это?

tgrosinger
источник
1
Использование JSON в качестве посредника в любом случае будет использовать отражение ... при условии, что вы собираетесь использовать encoding/jsonпакет stdlib для выполнения этого промежуточного шага ... Не могли бы вы привести пример карты и пример структуры, на которой можно было бы использовать этот метод?
Саймон Уайтхед
Да, именно по этой причине я стараюсь избегать JSON. Похоже, есть более эффективный метод, о котором я не знаю.
tgrosinger 04
Вы можете привести пример использования? Как в - показать какой-нибудь псевдокод, демонстрирующий, что будет делать этот метод?
Саймон Уайтхед
Ммм ... может быть, с unsafeпакетом есть способ ... но я не осмеливаюсь его пробовать. Помимо этого ... Требуется отражение, так как вам нужно иметь возможность запрашивать метаданные, связанные с типом, чтобы поместить данные в его свойства. Было бы довольно просто обернуть это в вызовы json.Marshal+ json.Decode... но это вдвое больше, чем отражение.
Саймон Уайтхед
Я удалил свой комментарий об отражении. Я больше заинтересован в том, чтобы делать это максимально эффективно. Если это означает использование отражения, это нормально.
tgrosinger 04

Ответы:

115

Самый простой способ - использовать https://github.com/mitchellh/mapstructure.

import "github.com/mitchellh/mapstructure"

mapstructure.Decode(myData, &result)

Если вы хотите сделать это самостоятельно, вы можете сделать что-то вроде этого:

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
Спасибо. Я использую немного измененную версию. play.golang.org/p/_JuMm6HMnU
tgrosinger
Я хочу, чтобы поведение FillStruct использовалось во всех моих различных структурах, и мне не приходилось определять его func (s MyStr...) FillStruct ...для каждой. Можно ли определить FillStruct для базовой структуры, чтобы все остальные мои структуры «унаследовали» это поведение? В приведенной выше парадигме это невозможно, поскольку только базовая структура ... в этом случае "MyStruct" фактически будет повторять свои поля
StartupGuy
Я имею в виду , вы могли бы его работать на любую структуру с чем - то вроде этого: play.golang.org/p/0weG38IUA9
Дэйв
Можно ли реализовать теги в Mystruct?
vicTROLLA
1
@abhishek, безусловно, есть штраф за производительность, который вы заплатите за сначала маршалинг в текст, а затем демаршалинг. Этот подход также, безусловно, проще. Это компромисс, и я бы выбрал более простое решение. Я ответил этим решением, потому что вопрос гласил: «Я знаю, что это можно сделать, используя JSON в качестве посредника; есть ли другой более эффективный способ сделать это?». Это решение будет более эффективным, решение JSON, как правило, будет проще реализовать и обдумать.
Дэйв
74

Библиотека https://github.com/mitchellh/mapstructure от Hashicorp делает это из коробки:

import "github.com/mitchellh/mapstructure"

mapstructure.Decode(myData, &result)

Второй resultпараметр должен быть адресом структуры.

Yunspace
источник
что, если ключ карты user_nameи struct filed есть UserName?
Николас Джела
1
@NicholasJela, он может справиться с этим с помощью тегов godoc.org/github.com/mitchellh/mapstructure#ex-Decode--Tags
Схема в стене
что, если карта kye - это _id, а имя карты - Id, тогда она не будет ее декодировать.
Рави Шанкар
27
  • самый простой способ сделать это - использовать encoding/jsonpackage

просто например:

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
Спасибо @jackytse. На самом деле это лучший способ сделать это !! структура карты часто не работает с вложенной картой в интерфейсе. Поэтому лучше рассмотреть интерфейс строки карты и обрабатывать его как json.
Жиль Эссоки
Go площадка ссылка на фрагменте кода выше: play.golang.org/p/JaKxETAbsnT
Junaid
13

Вы можете это сделать ... это может стать немного некрасивым, и вы столкнетесь с некоторыми пробами и ошибками с точки зрения типов сопоставления ... но вот основная суть этого:

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

Рабочий образец: http://play.golang.org/p/PYHz63sbvL

Саймон Уайтхед
источник
1
Это похоже на панику из-за нулевых значений:reflect: call of reflect.Value.Set on zero Value
Джеймс Тейлор
@JamesTaylor Да. Мой ответ предполагает, что вы точно знаете, какие поля вы сопоставляете. Если вам нужен аналогичный ответ с дополнительной обработкой ошибок (включая ошибку, с которой вы столкнулись), я бы предложил вместо этого ответ Дейвса.
Саймон Уайтхед
2

Я адаптирую ответ Дэйва и добавляю рекурсивную функцию. Я все еще работаю над более удобной версией. Например, числовая строка на карте должна иметь возможность преобразовывать в int в структуре.

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

Есть два шага:

  1. Конвертировать интерфейс в JSON Byte
  2. Преобразовать байт JSON в структуру

Ниже приведен пример:

dbByte, _ := json.Marshal(dbContent)
_ = json.Unmarshal(dbByte, &MyStruct)
Ник Л
источник