Суть проблемы
Есть интерфейс и есть несколько типов удовлетворяющих этому интерфейсу. Хочется сделать так, что бы можно было сохранить в JSON список таких интерфейсов а потом восстановить из JSON-а этот список.
Пример на геометрических фигурах
package geom import ( "math" ) type PlaneShape interface { Area() float64 Perimeter() float64 } type PlaneShapes []PlaneShape type Picture struct { Name string PlaneShapes []PlaneShape } type Point struct { X, Y float64 } type Line struct { X1, Y1, X2, Y2 float64 } type Rectangle struct { X1, Y1, X2, Y2 float64 } type Circle struct { X, Y, R float64 } func (f *Point) Area() float64 { return 0 } func (f *Line) Area() float64 { return 0 } func (f *Rectangle) Area() float64 { return math.Abs(f.X1-f.X2) * math.Abs(f.Y1-f.Y2) } func (f *Circle) Area() float64 { return math.Pi * f.R * f.R } func (f *Point) Perimeter() float64 { return 0 } func (f *Line) Perimeter() float64 { return math.Sqrt((f.X1-f.X2)*(f.X1-f.X2) + (f.Y1-f.Y2)*(f.Y1-f.Y2)) } func (f *Rectangle) Perimeter() float64 { return (math.Abs(f.X1-f.X2) + math.Abs(f.Y1-f.Y2)) * 2 } func (f *Circle) Perimeter() float64 { return math.Pi * f.R * 2 }
Хочется переопределить маршалинг и анмаршалинг для структур PlaneShapesи Picture
Как решить
Что бы получить типизацию, нужно добавить поле тип в сохраняемом JSON-е или сделать контейнер, который будет содержать тип и полезные данные. И при анмаршалинге использовать тип что бы получить нужную структуру.
примеры JSON
Контейнер:
{ "_type": "point", "data": { "x": 2, "y": 7.5, } }
Тип внутри структуры:
{ "_type": "point", "x": 2, "y": 7.5, }
Где _type - тип объекта
Тип решения
На GO можно решить такого рода проблему либо кодогенерацией, либо рефлексией либо комбинацией методов. Если решать вопрос рефлексией то нужно переписать весь механизм JSON. Это по сути ненужная задача, так как родной механизм достаточно хорош и есть неплохие альтернативы, которые хочется использовать при необходимости. По этому я решил попробовать решить задачу кодогенерацией. Так же не хочется усложнять маршалинг и анмаршалинг, так что в решении я буду использовать контейнерный подход.
Что будем использовать
Контейнер
Объекты будут маршалиться в контейнер:
//easyjson:json type IStructView struct { Type string `json:"_type"` Data json.RawMessage `json:"data"` }
Сразу сделаем, что бы маршалинг этого объекта был через easyjson потому, что он мне нравится.
Фабрика объектов
Каждый объект должен иметь метод который позволит получить тип объекта.
Что бы по типу получить нужный объект, нужно иметь фабрику объектов по их типу c методами:
Добавить генератор
Добавить генератор NIL объекта
Получить объект
Получить NIL объект
Реализация
type JsonInterfaceMarshaller interface { UnmarshalJSONTypeName() string } type StructFactory struct { Generators map[string]JsonUnmarshalObjectGenerate GeneratorsNil map[string]JsonUnmarshalObjectGenerate mx sync.RWMutex } var GlobalStructFactory = &StructFactory{ Generators: map[string]JsonUnmarshalObjectGenerate{}, GeneratorsNil: map[string]JsonUnmarshalObjectGenerate{}, } func (jsf *StructFactory) Add(name string, generator JsonUnmarshalObjectGenerate) { jsf.mx.Lock() defer jsf.mx.Unlock() jsf.Generators[name] = generator } func (jsf *StructFactory) AddNil(name string, generator JsonUnmarshalObjectGenerate) { jsf.mx.Lock() defer jsf.mx.Unlock() jsf.GeneratorsNil[name] = generator }
Что нужно генерировать
Для объекта который реализовывает интерфейс нужно добавить регистрацию в генераторе и добавить метод, который будет выдавать тип объекта.
func (obj *Point) UnmarshalJSONTypeName() string { return "geom.point" } func init() { mfj.GlobalStructFactory.Add("geom.point", func() mfj.JsonInterfaceMarshaller { return &Point{} }) mfj.GlobalStructFactory.AddNil("geom.point", func() mfj.JsonInterfaceMarshaller { var out *Point return out }) }
А для объектов содержащих поля с интерфейсными типами нужно описать методы MarshalJSON и UnmarshalJSON.
Что бы это сделать для каждого такого типа создадим прокси тип. Данные основной структуры будем записывать в прокси структуру, а данные из интерфейсных типов будем сохранять в виде IStructView. Получившуюся прокси структуру будем маршалить стандартными способами.
Пример определения прокси типов
type PlaneShapes_mjson_wrap []mfj.IStructView type Picture_mjson_wrap struct { Name string // PlaneShapes []PlaneShape PlaneShapes []mfj.IStructView }
Пример определения методов
type PlaneShapes_mjson_wrap []mfj.IStructView func (obj PlaneShapes) MarshalJSON() (res []byte, err error) { if obj == nil { var out PlaneShapes_mjson_wrap return json.Marshal(out) } out := make(PlaneShapes_mjson_wrap, len(obj)) swl := make([]mfj.IStructView, len(obj)) for i := 0; i < len(obj); i++ { if ujo, ok := obj[i].(mfj.JsonInterfaceMarshaller); ok { sw := mfj.IStructView{} sw.Type = ujo.UnmarshalJSONTypeName() sw.Data, err = json.Marshal(obj[i]) swl[i] = sw } else { swl[i] = mfj.IStructView{} } } return json.Marshal(out) } func (obj *PlaneShapes) UnmarshalJSON(data []byte) (err error) { if data == nil { return nil } var tmp PlaneShapes_mjson_wrap err = json.Unmarshal(data, &tmp) if err != nil { return err } if tmp == nil { var d PlaneShapes *obj = d return nil } objRaw := make(PlaneShapes, len(tmp)) *obj = objRaw for i := 0; i < len(tmp); i++ { if tmp[i].Type == "" { objRaw[i] = nil } else if tmp[i].Data == nil { to, er0 := mfj.GlobalStructFactory.GetNil(tmp[i].Type) if er0 != nil { return er0 } toTrans, ok := to.(PlaneShape) if !ok { return mft.ErrorS("Type 'PlaneShape' not valid in generations 'PlaneShapes' (ARR, NIL)") } objRaw[i] = toTrans } else { to, er0 := mfj.GlobalStructFactory.Get(tmp[i].Type) if er0 != nil { return er0 } toTrans, ok := to.(PlaneShape) if !ok { return mft.ErrorS("Type 'PlaneShape' not valid in generations 'PlaneShapes' (ARR)") } err = json.Unmarshal(tmp[i].Data, &toTrans) if err != nil { return err } objRaw[i] = toTrans } } return nil }
Как генерировать
На хабре есть классная статья про кодогенерацию тут.
Распарсим файл с именемfilename. Для этого будем использовать пакет go/token.
fset := token.NewFileSet() node, err := parser.ParseFile(fset, filename, nil, parser.ParseComments) if err != nil { return err }
Имя пакета лежит в node.Name.Name
В объекте поле node.Imports хранятся описания import из файла.
Если мы их будем перебирать, то
for _, imp := range node.Imports { // imp.Name - имя пакета если оно использовалось например так // log "github.com/sirupsen/logrus" if imp.Name != nil { // imp.Name.Name - само имя log } // imp.Path.Value - путь к пакету "github.com/sirupsen/logrus" // с кавычками }
В node.Decls хранятся описания объектов. Нас интересуют только *ast.GenDecl которые содержат информацию об описанных объектах.
for _, f := range node.Decls { genD, ok := f.(*ast.GenDecl) if !ok { continue } for _, spec := range genD.Specs { currType, ok := spec.(*ast.TypeSpec) if !ok { // Нас интересуют только типы continue } switch currType.Type.(type) { case *ast.StructType: // это описание структуры // например: // type ABCD struct { // A int // } case *ast.ArrayType: // это тип слайс // например: // type ABCDList []ABCD case *ast.MapType: // это тип мап // например: // type ABCDs map[string]ABCD default: // Это всё остальное } } if genD.Doc != nil && genD.Doc.List != nil { // вот тут содержится комментарий к объекту // например: // //sometext // type ABCDs map[string]ABCD for _, comment := range genD.Doc.List { if strings.HasPrefix(comment.Text, "//sometext") { // Что-то делать если есть коммент } } } // Разберём структуру currType, ok := spec.(*ast.TypeSpec) if !ok { continue } currStruct, ok := currType.Type.(*ast.StructType) if !ok { continue } // Пройдём по полям структуры for idxField, field := range currStruct.Fields.List { if len(field.Names) == 0 { continue } // field.Names[0].Name имя поля // field.Type тип поля if field.Tag != nil { // Получаем теги если есть tag := reflect.StructTag(field.Tag.Value[1 : len(field.Tag.Value)-1]) tagVal := tag.Get("json") // напрмер json } } }
Для разбора типов я написал функцию, которая получает что это за тип и список input-ов которые требуются.
Текст функции
func getType(at interface{}) (fieldType string, isArray bool, arrLen string, isMap bool, mapKeyType string, usedInputs map[string]struct{}) { usedInputs = make(map[string]struct{}) switch at.(type) { case *ast.Ident: fieldType = at.(*ast.Ident).Name case *ast.SelectorExpr: fieldType = at.(*ast.SelectorExpr).Sel.Name if expX, ok := at.(*ast.SelectorExpr).X.(*ast.Ident); ok { fieldType = expX.Name + "." + fieldType usedInputs[expX.Name] = struct{}{} } case *ast.StarExpr: subFieldType, subIsArray, subArrLen, subIsMap, subMapKeyType, subUsedInputs := getType(at.(*ast.StarExpr).X) for k := range subUsedInputs { usedInputs[k] = struct{}{} } switch { case !subIsArray && !subIsMap: fieldType = "*" + subFieldType case subIsArray: fieldType = "*[" + subArrLen + "]" + subFieldType case subIsMap: fieldType = "*map[" + subMapKeyType + "]" + subFieldType } case *ast.MapType: isMap = true { subFieldType, subIsArray, subArrLen, subIsMap, subMapKeyType, subUsedInputs := getType(at.(*ast.MapType).Key) for k := range subUsedInputs { usedInputs[k] = struct{}{} } switch { case !subIsArray && !subIsMap: mapKeyType = subFieldType case subIsArray: mapKeyType = "[" + subArrLen + "]" + subFieldType case subIsMap: mapKeyType = "map[" + subMapKeyType + "]" + subFieldType } } { subFieldType, subIsArray, subArrLen, subIsMap, subMapKeyType, subUsedInputs := getType(at.(*ast.MapType).Value) for k := range subUsedInputs { usedInputs[k] = struct{}{} } switch { case !subIsArray && !subIsMap: fieldType = subFieldType case subIsArray: fieldType = "[" + subArrLen + "]" + subFieldType case subIsMap: fieldType = "map[" + subMapKeyType + "]" + subFieldType } } case *ast.ArrayType: isArray = true if at.(*ast.ArrayType).Len != nil { arrLen = at.(*ast.ArrayType).Len.(*ast.BasicLit).Value } subFieldType, subIsArray, subArrLen, subIsMap, subMapKeyType, subUsedInputs := getType(at.(*ast.ArrayType).Elt) for k := range subUsedInputs { usedInputs[k] = struct{}{} } switch { case !subIsArray && !subIsMap: fieldType = subFieldType case subIsArray: fieldType = "[" + subArrLen + "]" + subFieldType case subIsMap: fieldType = "map[" + subMapKeyType + "]" + subFieldType } } return fieldType, isArray, arrLen, isMap, mapKeyType, usedInputs }
Результат
Написан инструмент для генерации кода для того, что бы маршалить и анмаршалить типы содержащие поля типом interface.
Для запуска генерации нужно написать mfjson file_name.go
Структуры нужно пометить вот так: //mfjson:interface struct_type_name, что бы добавить тип в фабрику объектов (что бы можно было использовать как интерфейс)
Структуры в которых нужно использовать интерфейсные типы в полях нужно пометить вот так: //mfjson:marshal , а для полей добавить атрибут mfjson:"true"
Код находится тут.
Пример использования ниже:
test_struct.go
package geom import ( "math" ) //go:generate mfjson test_struct.go type PlaneShape interface { Area() float64 Perimeter() float64 } //mfjson:marshal type PlaneShapes []PlaneShape //mfjson:marshal type Picture struct { Name string PlaneShapes []PlaneShape `json:"shapes" mfjson:"true"` } //mfjson:interface geom.point type Point struct { X, Y float64 } //mfjson:interface geom.line type Line struct { X1, Y1, X2, Y2 float64 } //mfjson:interface geom.rectangle type Rectangle struct { X1, Y1, X2, Y2 float64 } //mfjson:interface geom.circle type Circle struct { X, Y, R float64 } func (f *Point) Area() float64 { return 0 } func (f *Line) Area() float64 { return 0 } func (f *Rectangle) Area() float64 { return math.Abs(f.X1-f.X2) * math.Abs(f.Y1-f.Y2) } func (f *Circle) Area() float64 { return math.Pi * f.R * f.R } func (f *Point) Perimeter() float64 { return 0 } func (f *Line) Perimeter() float64 { return math.Sqrt((f.X1-f.X2)*(f.X1-f.X2) + (f.Y1-f.Y2)*(f.Y1-f.Y2)) } func (f *Rectangle) Perimeter() float64 { return (math.Abs(f.X1-f.X2) + math.Abs(f.Y1-f.Y2)) * 2 } func (f *Circle) Perimeter() float64 { return math.Pi * f.R * 2 }
test_struct.mfjson.go
// Code generated by mfjson for marshaling/unmarshaling. DO NOT EDIT. // https://github.com/myfantasy/json package geom import ( "encoding/json" "github.com/myfantasy/mft" mfj "github.com/myfantasy/json" ) type PlaneShapes_mjson_wrap []mfj.IStructView func (obj PlaneShapes) MarshalJSON() (res []byte, err error) { if obj == nil { var out PlaneShapes_mjson_wrap return json.Marshal(out) } out := make(PlaneShapes_mjson_wrap, len(obj)) swl := make([]mfj.IStructView, len(obj)) for i := 0; i < len(obj); i++ { if ujo, ok := obj[i].(mfj.JsonInterfaceMarshaller); ok { sw := mfj.IStructView{} sw.Type = ujo.UnmarshalJSONTypeName() sw.Data, err = json.Marshal(obj[i]) swl[i] = sw } else { swl[i] = mfj.IStructView{} } } return json.Marshal(out) } func (obj *PlaneShapes) UnmarshalJSON(data []byte) (err error) { if data == nil { return nil } var tmp PlaneShapes_mjson_wrap err = json.Unmarshal(data, &tmp) if err != nil { return err } if tmp == nil { var d PlaneShapes *obj = d return nil } objRaw := make(PlaneShapes, len(tmp)) *obj = objRaw for i := 0; i < len(tmp); i++ { if tmp[i].Type == "" { objRaw[i] = nil } else if tmp[i].Data == nil { to, er0 := mfj.GlobalStructFactory.GetNil(tmp[i].Type) if er0 != nil { return er0 } toTrans, ok := to.(PlaneShape) if !ok { return mft.ErrorS("Type 'PlaneShape' not valid in generations 'PlaneShapes' (ARR, NIL)") } objRaw[i] = toTrans } else { to, er0 := mfj.GlobalStructFactory.Get(tmp[i].Type) if er0 != nil { return er0 } toTrans, ok := to.(PlaneShape) if !ok { return mft.ErrorS("Type 'PlaneShape' not valid in generations 'PlaneShapes' (ARR)") } err = json.Unmarshal(tmp[i].Data, &toTrans) if err != nil { return err } objRaw[i] = toTrans } } return nil } type Picture_mjson_wrap struct { Name string // PlaneShapes []PlaneShape `json:"shapes" mfjson:"true"` PlaneShapes []mfj.IStructView `json:"shapes" mfjson:"true"` } func (obj Picture) MarshalJSON() (res []byte, err error) { out := Picture_mjson_wrap{} out.Name = obj.Name { if obj.PlaneShapes == nil { out.PlaneShapes = nil } else { swl := make([]mfj.IStructView, len(obj.PlaneShapes)) for i := 0; i < len(obj.PlaneShapes); i++ { if ujo, ok := obj.PlaneShapes[i].(mfj.JsonInterfaceMarshaller); ok { sw := mfj.IStructView{} sw.Type = ujo.UnmarshalJSONTypeName() sw.Data, err = json.Marshal(obj.PlaneShapes[i]) swl[i] = sw } else { swl[i] = mfj.IStructView{} } } out.PlaneShapes = swl } } return json.Marshal(out) } func (obj *Picture) UnmarshalJSON(data []byte) (err error) { tmp := Picture_mjson_wrap{} if data == nil { return nil } err = json.Unmarshal(data, &tmp) if err != nil { return err } obj.Name = tmp.Name { if tmp.PlaneShapes == nil { obj.PlaneShapes = nil } else { obj.PlaneShapes = make([]PlaneShape, len(tmp.PlaneShapes)) for i := 0; i < len(tmp.PlaneShapes); i++ { if tmp.PlaneShapes[i].Type == "" { obj.PlaneShapes[i] = nil } else if tmp.PlaneShapes[i].Data == nil { to, er0 := mfj.GlobalStructFactory.GetNil(tmp.PlaneShapes[i].Type) if er0 != nil { return er0 } toTrans, ok := to.(PlaneShape) if !ok { return mft.ErrorS("Type 'PlaneShapes' not valid in generations 'PlaneShape..Picture' (NIL)") } obj.PlaneShapes[i] = toTrans } else { to, er0 := mfj.GlobalStructFactory.Get(tmp.PlaneShapes[i].Type) if er0 != nil { return er0 } toTrans, ok := to.(PlaneShape) if !ok { return mft.ErrorS("Type 'PlaneShape' not valid in generations 'Picture..PlaneShapes'") } err = json.Unmarshal(tmp.PlaneShapes[i].Data, &toTrans) if err != nil { return err } obj.PlaneShapes[i] = toTrans } } } } return nil } func (obj *Point) UnmarshalJSONTypeName() string { return "geom.point" } func (obj *Line) UnmarshalJSONTypeName() string { return "geom.line" } func (obj *Rectangle) UnmarshalJSONTypeName() string { return "geom.rectangle" } func (obj *Circle) UnmarshalJSONTypeName() string { return "geom.circle" } func init() { mfj.GlobalStructFactory.Add("geom.point", func() mfj.JsonInterfaceMarshaller { return &Point{} }) mfj.GlobalStructFactory.AddNil("geom.point", func() mfj.JsonInterfaceMarshaller { var out *Point return out }) mfj.GlobalStructFactory.Add("geom.line", func() mfj.JsonInterfaceMarshaller { return &Line{} }) mfj.GlobalStructFactory.AddNil("geom.line", func() mfj.JsonInterfaceMarshaller { var out *Line return out }) mfj.GlobalStructFactory.Add("geom.rectangle", func() mfj.JsonInterfaceMarshaller { return &Rectangle{} }) mfj.GlobalStructFactory.AddNil("geom.rectangle", func() mfj.JsonInterfaceMarshaller { var out *Rectangle return out }) mfj.GlobalStructFactory.Add("geom.circle", func() mfj.JsonInterfaceMarshaller { return &Circle{} }) mfj.GlobalStructFactory.AddNil("geom.circle", func() mfj.JsonInterfaceMarshaller { var out *Circle return out }) }
