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







В части 3 был представлен шаблон, посредством которого мы вводили поток с полем «$type», что позволяло нам реконструировать поток в типы нашего интерфейса. Наше решение основано на процессе создания потока, включая обязательное поле с правильным типом. В этой статье мы решили автоматизировать это и разрешить реализацию интерфейса Go в оба конца. Весь код установки одинаков. Хотя вы могли заметить, что теперь мы включаем пакет Reflect.
основной пакет

package main

import (
   "encoding/json"
   "fmt"
   "reflect"
   "strings"
)

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"`
}

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)
type MyList []MyInterface

Наша цель — иметь возможность запускать следующий код без паники.

func main() {
   l := MyList{
      StructA{A: 1.23},
      StructA{A: 3.45},
      StructB{B: "hello"},
   }
   b, _ := json.Marshal(l)
   var nl MyList
   _ = json.Unmarshal(b, &nl)
   fmt.Println(l)
   fmt.Println(nl)
   if fmt.Sprint(l) != fmt.Sprint(nl) {
      panic("Unable to round-trip")
   }
   fmt.Println(string(b))
}

На боковой панели вы заметите, что я выполняю сравнение строк, чтобы подтвердить, что созданные типы одинаковы. Я делаю это, поскольку между срезами нет стандартного оператора сравнения. В «реальном» коде я бы использовал собственную реализацию сравнения.

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



Во-первых, давайте создадим подпрограмму, которая, учитывая экземпляр нашего типа интерфейса, создает поток JSON со встроенным полем «$ type». Наивный способ, ну, способ с наибольшей типизацией, состоял бы в том, чтобы реализовать собственный метод MarshalJSON для каждой реализации. Создать пользовательскую функцию несложно, но это несколько подвержено ошибкам, поскольку мне нужно добавить поле в структуру, а этому полю нужен тег структуры, чтобы правильно отобразить его в потоке.

func (t StructA)MarshalJSON() ([]byte, error) {
   return json.Marshal( struct { 
      A float64
      Type string `json:"$type"`
   }{
      A: t.A,
      Type: t.Type(),
   })
}


В частности, теги, определенные для потоковой передачи JSON, задокументированные в Marshal



Кодирование потока

После этого мы рассмотрим более общее решение, и код следует за ним.

func LazyEncode(item MyInterface) ([]byte, error) {
   elements := make(map[string]json.RawMessage)
   val := reflect.Indirect(reflect.ValueOf(item))
   typ := val.Type()
   // iterate through the fields of the struct
   for i := 0; i < typ.NumField(); i++ {
      if typ.Field(i).IsExported() {
         fieldValue := val.Field(i)
         tag := typ.Field(i).Name
         b, err := json.Marshal(fieldValue.Interface())
         if err != nil {
            return nil, err
         }
         elements[tag] = b
      }
   }
   // Encode the type string
   t, err := json.Marshal(item.Type())
   if err != nil {
      return nil, err
   }
   elements["$type"] = t
   // Finally, marshal the entire result
   return json.Marshal(elements)
}

Итак, что здесь происходит? Во-первых, мы используем немного «магии» из пакета JSON; это json.RawMessage, который мы использовали в предыдущих разделах. Только в этом случае мы делаем карту из строки в json.RawMessage. Мы будем использовать эту карту для хранения представления потока для каждого из экспортируемых полей нашей структуры.

elements := make(map[string]json.RawMessage)

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

Используя отражение, мы получаем значение и тип поля, и обратите внимание, что здесь мы используем `reflect.Indirect`, так что нам не нужно беспокоиться о том, является ли переданный тип указателем или значением.

val := reflect.Indirect(reflect.ValueOf(item))
typ := val.Type()

Затем мы перебираем поля в нашей структуре, превращая каждое из них в байтовый срез, используя стандартный метод json.Marshal.

for i := 0; i < typ.NumField(); i++ {
      if typ.Field(i).IsExported() {
         fieldValue := val.Field(i)
         tag := typ.Field(i).Name
         b, err := json.Marshal(fieldValue.Interface())
         if err != nil {
            return nil, err
         }
         elements[tag] = b
      }
   }

Теперь у нас есть карта, заполненная каждым полем нашей структуры. Наконец, мы добавляем наше пользовательское поле «$ type» и маршалируем результат как JSON.

[{“$type”:”StructA”,”A”:1.23},{“$type”:”StructA”,”A”:3.45},{“$type”:”StructB”,”B”:”hello”}]

Очевидно, что для производственного кода мы сейчас пишем несколько тестов, чтобы убедиться, что эта функция ведет себя правильно. Мы также можем обновить нашу функцию «MyList» MarshalJSON, чтобы использовать этот новый код.

func (l MyList) MarshalJSON() ([]byte, error) {
   raw := make([]json.RawMessage, len(l))
   for i, v := range l {
      b, err := LazyEncode(v)
      if err != nil {
         return nil, err
      }
      raw[i] = b
   }
   return json.Marshal(raw)
}

В идеальном мире нам не пришлось бы использовать отложенное декодирование JSON, но поскольку мы не можем расширить реализацию кодирования JSON по умолчанию, мы должны сделать это в нашем коде. Другой способ, которым мы могли бы добиться этого, — настроить каждую из наших реализаций.

func (t StructA) MarshalJSON() ([]byte, error) {
   return LazyEncode( t )
}

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

{“$type”:”StructA”,”A”:1.23}

Декодирование потока

Только что созданный нами поток напрямую совместим с потоком, читаемым в части 3.



Таким образом, мы могли бы остановиться здесь, но, надеюсь, логика кодирования потока подсказала решение. Мы собираемся выполнить обратную операцию и определить функцию

func LazyDecode(b []byte) (MyInterface, error) {...}

Это использует аналогичную логику отражения, чтобы превратить наш поток байтов в реализацию нашего интерфейса.

Если вы оглянетесь назад на то, как мы определили New, мы должны были вернуть указатель на новый экземпляр нашего типа. Такое поведение — одна из самых запутанных областей Go, где интерфейс может указывать на значение типа или указатель на тип. Невозможно обеспечить, чтобы интерфейс, определенный с приемниками значений, содержал только значение. Оба следующих являются правильными реализациями интерфейса.

var foo MyInterface = StructA{A: 1}
var bar MyInterface = &StructA{A: 2}

Если я распечатаю их с помощью fmt.Println, я получу следующее

{1}
&{2}

Это имеет смысл, когда вы думаете об этом, но это может вернуться, чтобы укусить вас. Вот моя функция LazyDecode; Я прокомментировал функцию, и она является зеркальным отражением LazyEncode.

func LazyDecode(b []byte) (MyInterface, error) {
   // Create a map from string to RawMessage
   // when we Unmarshal to this map the elements
   // will not be decoded
   elements := make(map[string]json.RawMessage)
   err := json.Unmarshal(b, &elements)
   if err != nil {
      return nil, err
   }
   // Check that we have our type field in the stream
   if typ, ok := elements["$type"]; ok {
      // Now we decode the type into a string
      var t string
      err := json.Unmarshal(typ, &t)
      if err != nil {
         return nil, err
      }
      // Create a new instance of the type
      myInterfaceFunc, ok := lookup[Type(t)]
      // use reflection to iterate the
      if !ok {
         return nil, fmt.Errorf("unregistered interface type : %s", t)
      }
      item := myInterfaceFunc.New()
      // Due to the way we defined New we have a pointer to our type
      val := reflect.Indirect(reflect.ValueOf(item))
      typ := val.Type()
      // iterate through the fields of the struct
      for i := 0; i < typ.NumField(); i++ {
         if typ.Field(i).IsExported() {
            fieldValue := val.Field(i)
            // Get the field from stream and decode it 
            // into our structure
            tag := typ.Field(i).Name
            elem, ok := elements[tag]
            if !ok {
               // Fall back to lowercase, 
               // needs to be case insensitive
               elem, ok = elements[strings.ToLower(tag)]
            }
            if ok {
               err := json.Unmarshal(elem, fieldValue.Addr().Interface())
               if err != nil {
                  return nil, err
               }
            } else {
               fmt.Printf("Warn: tag %s not found in stream", tag)
               // FIXME, we need structure tags to tell if this is empty or not
            }
         }
      }
      return item, nil
   }
   return nil, fmt.Errorf("invalid stream, no $type specified")
}

Когда мы запускаем этот код в нашем сообщении JSON, мы получаем следующее

[0x14000018218 0x14000018268 0x14000010360]

У нас есть список указателей на нашу реализацию. Этот результат не является неверным, но он не является допустимым круговым обходом. Мы ожидали, что наш код не будет паниковать.

[{1.23} {3.45} {hello}]

Создание результата на основе ценности

Мы снова обращаемся к размышлению; на этот раз мы используем информацию о нашем типе для создания новой версии типа на основе значений. К счастью, решение нашей проблемы — это изменение в одну строку! Вместо того, чтобы напрямую возвращать наш элемент, мы создаем тип значения. Мы должны привести это к типу MyInterface, так как .Interface() возвращает интерфейс {}, но это безопасно, поскольку мы знаем, что поддерживаются только типы, реализующие MyInterface.

return val.Interface().(MyInterface), nil

Теперь мы получаем ожидаемый результат

func main() {
   l := MyList{
      StructA{A: 1.23},
      StructA{A: 3.45},
      StructB{B: "hello"},
   }
   b, _ := json.Marshal(l)
   var nl MyList
   _ = json.Unmarshal(b, &nl)
   fmt.Println(l)
   fmt.Println(nl)
   if fmt.Sprint(l) != fmt.Sprint(nl) {
      panic("Unable to round-trip")
   }
   fmt.Println(string(b))
}

генерирует следующий вывод и больше не паникует.

[{1.23} {3.45} {hello}]
[{1.23} {3.45} {hello}]
[{“$type”:”StructA”,”A”:1.23},{“$type”:”StructA”,”A”:3.45},{“$type”:”StructB”,”B”:”hello”}]

Полный код

package main

import (
   "encoding/json"
   "fmt"
   "reflect"
   "strings"
)

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"`
}

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)

type MyList []MyInterface

func LazyEncode(item MyInterface) ([]byte, error) {
   elements := make(map[string]json.RawMessage)
   val := reflect.Indirect(reflect.ValueOf(item))
   typ := val.Type()
   // iterate through the fields of the struct
   for i := 0; i < typ.NumField(); i++ {
      if typ.Field(i).IsExported() {
         fieldValue := val.Field(i)
         // Get the field from stream and decode it into out structure
         tag := typ.Field(i).Name
         b, err := json.Marshal(fieldValue.Interface())
         if err != nil {
            return nil, err
         }
         elements[tag] = b
      }
   }
   // Encode the type string
   t, err := json.Marshal(item.Type())
   if err != nil {
      return nil, err
   }
   elements["$type"] = t
   // Finally, marshal the entire result
   return json.Marshal(elements)
}

func LazyDecode(b []byte) (MyInterface, error) {
   // Create a map from string to RawMessage
   // when we Unmarshal to this map the elements
   // will not be decoded
   elements := make(map[string]json.RawMessage)
   err := json.Unmarshal(b, &elements)
   if err != nil {
      return nil, err
   }
   // Check that we have our type field in the stream
   if typ, ok := elements["$type"]; ok {
      // Now we decode the type into a string
      var t string
      err := json.Unmarshal(typ, &t)
      if err != nil {
         return nil, err
      }
      // Create a new instance of the type
      myInterfaceFunc, ok := lookup[Type(t)]
      // use reflection to iterate the
      if !ok {
         return nil, fmt.Errorf("unregistered interface type : %s", t)
      }
      item := myInterfaceFunc.New()
      // Due to the way we defined New we have a pointer to our type
      val := reflect.Indirect(reflect.ValueOf(item))
      typ := val.Type()
      // iterate through the fields of the struct
      for i := 0; i < typ.NumField(); i++ {
         if typ.Field(i).IsExported() {
            fieldValue := val.Field(i)
            // Get the field from stream and decode it into out structure
            tag := typ.Field(i).Name
            elem, ok := elements[tag]
            if !ok {
               // Fall back to lowercase, needs to be case insensitive
               elem, ok = elements[strings.ToLower(tag)]
            }
            if ok {
               err := json.Unmarshal(elem, fieldValue.Addr().Interface())
               if err != nil {
                  return nil, err
               }
            } else {
               fmt.Printf("Warn: tag %s not found in stream", tag)
               // FIXME, we need structure tags to tell if this is empty or not
            }
         }
      }
      return val.Interface().(MyInterface), nil
   }
   return nil, fmt.Errorf("invalid stream, no $type specified")
}

func (l *MyList) UnmarshalJSON(b []byte) error {
   var raw []json.RawMessage
   err := json.Unmarshal(b, &raw)
   if err != nil {
      return err
   }
   // Allocate an array of MyInterface
   *l = make(MyList, len(raw))
   for i, r := range raw {
      item, err := LazyDecode(r)
      if err != nil {
         return err
      }
      (*l)[i] = item
   }
   return nil
}

func (l MyList) MarshalJSON() ([]byte, error) {
   raw := make([]json.RawMessage, len(l))
   for i, v := range l {
      b, err := LazyEncode(v)
      if err != nil {
         return nil, err
      }
      raw[i] = b
   }
   return json.Marshal(raw)
}

func main() {
   l := MyList{
      StructA{A: 1.23},
      StructA{A: 3.45},
      StructB{B: "hello"},
   }
   b, _ := json.Marshal(l)
   var nl MyList
   _ = json.Unmarshal(b, &nl)
   fmt.Println(l)
   fmt.Println(nl)
   if fmt.Sprint(l) != fmt.Sprint(nl) {
      panic("Unable to round-trip")
   }
   fmt.Println(string(b))
}

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