В первой части этой короткой серии я рассмотрел основные методы, необходимые для потоковой передачи типа интерфейса в JSON и из него в Go. Раньше я обещал, что почищу функции, никому не нравится оператор if в их потоковом коде, и я расскажу о том, почему рефлексия не особо помогает. Я также попытаюсь убедить вас, что не использовать рефлексию не так уж и плохо.
Если вы не читали первую часть, остановитесь здесь и прочитайте
Итак, я предполагаю, что вы выполнили шаги и теперь у вас есть работающий обход для пары структур. Одним из моментов, который я упустил из виду в первой части, был выбор типа, используемого в определении интерфейса.
type Type string type MyInterface interface { Type() Type }
Я создаю псевдоним типа для строки и простую функцию, которую нужно реализовать, которая возвращает этот тип. Если вы следили за этим, вы также заметите, что я использовал этот тип в потоковой версии структуры.
func (x StructX) MarshalJSON() ([]byte, error) { var xr struct { X string `json:"x"` MyInterface MyInterface `json:"my_interface"` MyInterfaceType Type `json:"my_interface_type"` } xr.X = x.X xr.MyInterface = x.MyInterface xr.MyInterfaceType = x.MyInterface.Type() return json.Marshal(xr) }
а еще в декодере я вручную перечислил эти типы. Это явно подвержено ошибкам, но без какой-либо формы поиска это лучшее, что вы можете сделать. Итак, теперь мы добавляем простую карту поиска
var lookup = make( map[Type]MyInterface )
И функция, которую мы можем использовать для регистрации наших типов
func Register(iface MyInterface) { lookup[iface.Type()] = iface }
Эта карта является глобальной, поэтому любые мутации должны быть защищены, но, как вы увидите, мы изменяем эту карту только во время запуска модуля, а среда выполнения Go гарантирует, что порядок вызовов инициализации правильный.
func init() { Register( StructA{} ) Register( StructB{} ) }
Теперь, когда мы впервые импортируем модуль или запускаем нашу основную функцию, вызываются функции инициализации, и наша карта заполняется. Теперь мы можем переопределить декодер как
func (x *StructX) UnmarshalJSON(b []byte) error { var xr struct { X string `json:"x"` MyInterface json.RawMessage `json:"my_interface"` MyInterfaceType Type `json:"my_interface_type"` } err := json.Unmarshal(b, &xr) if err != nil { return err } x.X = xr.X myInterface, ok := lookup[xr.MyInterfaceType] if !ok { return fmt.Errorf("unregistered interface type : %s", xr.MyInterfaceType) } err = json.Unmarshal(xr.MyInterface, myInterface) if err != nil { return err } x.MyInterface = a return nil }
Это выглядит великолепно, но не работает! Мы сталкиваемся с той же проблемой, что и раньше
panic: json: Unmarshal(non-pointer main.StructA)
Это опять та самая двойственность интерфейса указателя. Проблема в том, что наша карта имеет запись на основе значений, и нам нужен указатель, но если мы сохраним указатель, мы будем совместно использовать один и тот же экземпляр интерфейса. В этот момент у меня начинает кружиться голова. Есть несколько способов решить эту проблему. Мы можем положиться на отражение и создать новый экземпляр класса, мы можем добавить еще один метод, который возвращает новый экземпляр типа, или мы можем использовать лямбда-функцию для возврата нового значения. Возможно, проще всего добавить еще один метод в наш интерфейс.
type Type string type MyInterface interface { Type() Type New() MyInterface } var lookup = make(map[Type]MyInterface) func Register(iface MyInterface) { lookup[iface.Type()] = iface }
и теперь мы реализуем функции
func (_ StructA) New() MyInterface { return &StructA{} } func (_ StructB) New() MyInterface { return &StructB{} }
и обнови декодер
func (x *StructX) UnmarshalJSON(b []byte) error { var xr struct { X string `json:"x"` MyInterface json.RawMessage `json:"my_interface"` MyInterfaceType Type `json:"my_interface_type"` } err := json.Unmarshal(b, &xr) if err != nil { return err } x.X = xr.X myInterfaceFunc, ok := lookup[xr.MyInterfaceType] if !ok { return fmt.Errorf("unregistered interface type : %s", xr.MyInterfaceType) } myInterface := myInterfaceFunc.New() err = json.Unmarshal(xr.MyInterface, myInterface) if err != nil { return err } x.MyInterface = myInterface return nil }
и теперь все это работает, мы можем регистрировать новые типы потоковой передачи и создавать их автоматически. Обратите внимание, что вы по-прежнему должны быть осторожны, так как нет прямого способа гарантировать, что интерфейс указывает на указатель или значение. Вы можете сделать это с отражением, но это создает более сложную среду.
Вы спросите, а почему вы не использовали рефлексию? В отличие от отражения в Java и некоторых других языках, вы не можете просто создать экземпляр типа по имени. Вы должны сначала зарегистрировать эти типы на карте, чтобы вы могли дублировать тип. Вот почему для API golang gob требуется функция регистрации.
с этой реализацией
https://cs.opensource.google/go/go/+/refs/tags/go1.19:src/encoding/gob/type.go;l=836
Поскольку здесь мы имеем полный контроль, немного понятнее использовать дополнительный метод в интерфейсе.
Вот и все, теперь мы можем выполнять обмен интерфейсами с JSON и изучили отложенное декодирование и способы создания типов по имени.
Полный код следует
package main import ( "encoding/json" "fmt" ) type Type string type MyInterface interface { Type() Type New() MyInterface } var lookup = make(map[Type]MyInterface) func Register(iface MyInterface) { lookup[iface.Type()] = iface } func init() { Register(StructA{}) Register(StructB{}) } type StructA struct { A float64 `json:"a"` } type StructB struct { B string `json:"b"` } type StructX struct { X string `json:"x"` MyInterface MyInterface `json:"my_interface"` } type StructXRAW struct { X string `json:"x"` MyInterface json.RawMessage `json:"my_interface"` } func (_ StructA) Type() Type { return "StructA" } func (_ StructB) Type() Type { return "StructB" } func (_ StructA) New() MyInterface { return &StructA{} } func (_ StructB) New() MyInterface { return &StructB{} } // Check that we have implemented the interface var _ MyInterface = (*StructA)(nil) var _ MyInterface = (*StructB)(nil) func (x StructX) MarshalJSON() ([]byte, error) { var xr struct { X string `json:"x"` MyInterface MyInterface `json:"my_interface"` MyInterfaceType Type `json:"my_interface_type"` } xr.X = x.X xr.MyInterface = x.MyInterface xr.MyInterfaceType = x.MyInterface.Type() return json.Marshal(xr) } func (x *StructX) UnmarshalJSON(b []byte) error { var xr struct { X string `json:"x"` MyInterface json.RawMessage `json:"my_interface"` MyInterfaceType Type `json:"my_interface_type"` } err := json.Unmarshal(b, &xr) if err != nil { return err } x.X = xr.X myInterfaceFunc, ok := lookup[xr.MyInterfaceType] if !ok { return fmt.Errorf("unregistered interface type : %s", xr.MyInterfaceType) } myInterface := myInterfaceFunc.New() err = json.Unmarshal(xr.MyInterface, myInterface) if err != nil { return err } x.MyInterface = myInterface return nil } func main() { // Create an instance of each a turn to JSON xa := StructX{X: "xyz", MyInterface: StructA{A: 1.23}} xb := StructX{X: "xyz", MyInterface: StructB{B: "hello"}} xaJSON, _ := json.Marshal(xa) xbJSON, _ := json.Marshal(xb) println(string(xaJSON)) println(string(xbJSON)) var newX StructX err := json.Unmarshal(xaJSON, &newX) if err != nil { panic(err) } err = json.Unmarshal(xbJSON, &newX) if err != nil { panic(err) } }