Я сделал пример с вашим кодом 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