Рефлексия и обработка всех структур определенного типа

Рейтинг: 0Ответов: 1Опубликовано: 19.05.2023

Пытаюсь реализовать пост-процессинг в проекте, и перед сериализацией в JSON сделать обработку структуры (для ответов API используются разные структуры) с целью конверсии валюты в заданную.

Имеется:

Структура, содержащая цену с валютой

 type Price struct {
        Amount decimal.Decimal `json:"Amount"`
        Currency string `json:"Currency`"
    }

Структура ответа #1:

type ExampleRS struct {
    Total price.Price `json:"Total"`
}

Дополнительная структура для ответа #2

type ResponseEntry struct {
   ExampleIntValue int `json:"ExampleValue"`
   ExampleStringArrayValues []string `json:"ExampleStringArrayValues"`
   Price price.Price `json:"Price"`
}

Структура ответа #2

  type ExampleListRS struct {
        Data []ResponseEntry `json:"Response"`
        PerPage uint `json:"Count"`
        Pages uint `json:"Pages"`
    }

Вопрос - как реализовать метод, принимающий на вход interface{}, или reflect.ValueOf вместо конкретного типа (ExampleListRS или ExampleRS), который будет рекурсивно проходить по структуре, делая проверку на price.Price, и умножающий в ней decimal.Decimal скажем, на 5?

Пробую реализовать через пример, но не понимаю как сравнивать (и что) с типом price.Price.

Ответы

▲ 1Принят

Я сделал пример с вашим кодом https://github.com/pakuula/StackOverflow/tree/main/go/1520699

UPDATE: РЕКУРСИЯ

Обновил пример, добавил поддержку рекурсии.

Шаблон Visitor обходит значения и ищет значение типа price.Price. Когда находит, умножает цену на заданное число, и продолжает искать дальше.


func IsPrice(t reflect.Type) bool {
    return t == reflect.TypeOf(price.Price{})
}

type Visitor struct {
    Factor decimal.Decimal
}

func (v *Visitor) Process(val interface{}) {
    v.Accept(reflect.ValueOf(val))
}
func (v *Visitor) Accept(val reflect.Value) {
    switch val.Kind() {
    case reflect.Array:
    case reflect.Slice:
        for i := 0; i < val.Len(); i++ {
            v.Accept(val.Index(i))
        }
    case reflect.Map:
        for _, key := range val.MapKeys() {
            v.Accept(val.MapIndex(key))
        }
    case reflect.Pointer:
        if val.IsNil() {
            return
        }
        v.Accept(val.Elem())
    case reflect.Struct:
        if IsPrice(val.Type()) {
            amtField := val.FieldByName("Amount")
            amt := amtField.Interface().(decimal.Decimal)
            amt = amt.Mul(v.Factor)
            amtField.Set(reflect.ValueOf(amt))
            return
        }
        for i := 0; i < val.NumField(); i++ {
            v.Accept(val.Field(i))
        }
    default:
        return
    }
}

Обработка json строки:

var defaultVisitor = Visitor{
    Factor: decimal.NewFromInt32(5),
}

func unmarshalJson(jsonDoc []byte) (interface{}, error) {
    for _, t := range []reflect.Type{
        reflect.TypeOf(ExampleRS{}),
        reflect.TypeOf(ExampleListRS{}),
    } {
        val := reflect.New(t)
        decoder := json.NewDecoder(bytes.NewReader(jsonDoc))
        decoder.DisallowUnknownFields()
        err := decoder.Decode(val.Interface())
        if err == nil {
            defaultVisitor.Accept(val)
            return val.Elem().Interface(), nil
        }
    }
    return nil, fmt.Errorf("unknown JSON structure: %s", string(jsonDoc))
}

В этой функции самое хитрое - вызов decoder.DisallowUnknownFields(). Если это не сделать, то декодер будет парсить все документы как значение первого типа, игнорируя несоответствия.

Пример:

func demo3() {
    amt1, _ := decimal.NewFromString("12.345")
    amt2, _ := decimal.NewFromString("54.321")
    test := ExampleListRS{
        Data: []ResponseEntry{
            {
                ExampleIntValue:          0,
                ExampleStringArrayValues: []string{"Helo"},
                Price: price.Price{
                    Amount:   amt1,
                    Currency: "BTC",
                },
            },
            {
                ExampleIntValue:          1,
                ExampleStringArrayValues: []string{"world"},
                Price: price.Price{
                    Amount:   amt2,
                    Currency: "ETH",
                },
            },
        },
        PerPage: 10,
        Pages:   2,
    }
    jsonDoc, _ := json.Marshal(test)
    fmt.Println("Before: ", test)
    val, err := unmarshalJson(jsonDoc)
    if err != nil {
        fmt.Println("failed: ", err.Error())
    } else {
        fmt.Printf("After: %v\n", val)
    }
}

Вывод:

Before:  {[{0 [Helo] {12.345 BTC}} {1 [world] {54.321 ETH}}] 10 2}
After: {[{0 [Helo] {61.725 BTC}} {1 [world] {271.605 ETH}}] 10 2}

То есть функция unmarshalJson правильно определила тип структуры, а обходчик поправил все найденные значения price.Price

ПЕРВОНАЧАЛЬНЫЙ ОТВЕТ

Проверку типа нужно делать по имени. В моём примере корневой пакет приложения example.org/reflect, тип Price определён во вложенном пакете price. Поэтому его полное имя "example.org/reflect/price".Price и проверка выглядит так:

func IsPrice(t reflect.Type) bool {
    return t.PkgPath() == "example.org/reflect/price" && t.Name() == "Price"
}

Соответственно, поиск поля с типом price.Price можно сделать так:

func HasPrice(response interface{}) (bool, *reflect.Value) {
    val := reflect.ValueOf(response)
    for val.Kind() == reflect.Pointer {
        val = val.Elem()
    }
    if val.Kind() != reflect.Struct {
        return false, nil
    }
    for i := 0; i < val.NumField(); i++ {
        field := val.Field(i)
        if IsPrice(field.Type()) {
            return true, &field
        }
    }
    return false, nil
}

Как поменять значение поля


func main() {
    amt, _ := decimal.NewFromString("12.345")
    test := ExampleRS{
        Total: price.Price{
            Amount:   amt,
            Currency: "BTC",
        },
    }
    fmt.Println("Before: ", test)
    has, field := HasPrice(&test)
    if has {
        amtField := field.FieldByName("Amount")
        amt := amtField.Interface().(decimal.Decimal)
        amt = amt.Mul(decimal.NewFromInt32(5))
        amtField.Set(reflect.ValueOf(amt))
    }
    fmt.Println("After:", test)
}

Результат:

Before:  {{12.345 BTC}}
After: {{61.725 BTC}}

Здесь есть тонкий момент - с каким аргументом вызывать HasPrice. Если вызвать HasPrice(test) без указателя, то будет вызвана функция для временного объекта, и в этом случае amtField.Set запаникует reflect.Value.Set using unaddressable value

Поэтому вызывать нужно от указателя HasPrice(&test). В этом случае компилятор разместит test в куче и это будет глобальный объект. Поля глобальных объектов можно изменять через reflect