Golang解析类型为interface的切片

huangapple go评论84阅读模式
英文:

Golang Unmarshal slice of type interface

问题

在这个例子中,我将尝试加载包含多边形的2D场景。在代码中,我将有许多不同的结构体,如Circle、Square、Rectangle、Pentagon等等。所有这些结构体都会共享一些公共的方法,比如Area和Perimeter。场景本身将被存储为接口类型Polygon的切片。

以下是我用来测试的代码:

package main

import (
	"encoding/json"
	"fmt"
	"math"
)

type Polygon interface {
	Area() float32
}

type Rectangle struct {
	Base   float32 `json:"base"`
	Height float32 `json:"height"`
	X      float32 `json:"x"`
	Y      float32 `json:"y"`
}

func (r *Rectangle) Area() float32 {
	return r.Base * r.Height
}

type Circle struct {
	Radius float32 `json:"radius"`
	X      float32 `json:"x"`
	Y      float32 `json:"y"`
}

func (c *Circle) Area() float32 {
	return c.Radius * c.Radius * math.Pi
}

func main() {
	rect := Rectangle{Base: 10, Height: 10, X: 10, Y: 10}
	circ := Circle{Radius: 10, X: 0, Y: 0}

	sliceOfPolygons := make([]Polygon, 0, 2)

	sliceOfPolygons = append(sliceOfPolygons, &rect, &circ)

	jsonData, err := json.Marshal(sliceOfPolygons)
	if err != nil {
		panic(err)
	}

	fmt.Println(string(jsonData))

	newSlice := make([]Polygon, 0)

	err = json.Unmarshal(jsonData, &newSlice)
	if err != nil {
		panic(err)
	}
}

在这个例子中,我设置了一个包含2个多边形的切片,对其进行了编组(marshal),然后尝试再次进行解组(unmarshal)。编组后的字符串为:

[{"base":10,"height":10,"x":10,"y":10},{"radius":10,"x":0,"y":0}]

但是当我尝试进行Unmarshal时,会出现错误:

panic: json: cannot unmarshal object into Go value of type main.Polygon

如果这个能够工作,那将非常有用且简单易用。我认为Unmarshal无法从JSON字符串中区分出RectangleCircle,因此它无法知道应该构建哪个结构体。

有没有办法为结构体添加标签或告诉Unmarshal如何区分这些结构体呢?

英文:

In this example, I would be trying to load 2D scenes that contain polygons. In the code, I would have many different structs such as Circle, Square, Rectangle, Pentagon, etc..
All would share common funcs, such as Area and Perimeter.
The scene itself would be stored as a slice of the interface Polygon.

Here is the code I'm using to test this:

package main

import (
	"encoding/json"
	"fmt"
	"math"
)

type Polygon interface {
	Area() float32
}

type Rectangle struct {
	Base   float32 `json:"base"`
	Height float32 `json:"height"`
	X      float32 `json:"x"`
	Y      float32 `json:"y"`
}

func (r *Rectangle) Area() float32 {
	return r.Base * r.Height
}

type Circle struct {
	Radius float32 `json:"radius"`
	X      float32 `json:"x"`
	Y      float32 `json:"y"`
}

func (c *Circle) Area() float32 {
	return c.Radius * c.Radius * math.Pi
}

func main() {
	rect := Rectangle{Base: 10, Height: 10, X: 10, Y: 10}
	circ := Circle{Radius: 10, X: 0, Y: 0}

	sliceOfPolygons := make([]Polygon, 0, 2)

	sliceOfPolygons = append(sliceOfPolygons, &rect, &circ)

	jsonData, err := json.Marshal(sliceOfPolygons)
	if err != nil {
		panic(err)
	}

	fmt.Println(string(jsonData))

	newSlice := make([]Polygon, 0)

	err = json.Unmarshal(jsonData, &newSlice)
	if err != nil {
		panic(err)
	}
}

In this example, I setup a slice of 2 polygons, marshal it and then try to unmarshal it again.
The marshal-ed string is:

[{"base":10,"height":10,"x":10,"y":10},{"radius":10,"x":0,"y":0}]

But when I try to Unmarshal it panics:

panic: json: cannot unmarshal object into Go value of type main.Polygon

If this worked it would be really useful and simple to use. I'd say that Unmarshall can't distinguish between a Rectangle and a Circle from the json string so it can't possibly know what struct to build.

Is there any way to tag the struct or tell Unmarshal how to distinguish this structs?

答案1

得分: 1

思路是通过区分JSON是圆形还是矩形来进行识别。在你的JSON中,没有用于检测对象类型的标识。所以我们可以制定规则。

  • 矩形的底边和高度都大于0
  • 圆形的半径大于0

为了解析JSON,它应该具有以下常见字段。

type Object struct {
    Base   float32 `json:"base,omitempty"`
    Radius float32 `json:"radius,omitempty"`
    Height float32 `json:"height,omitempty"`
    X      float32 `json:"x"`
    Y      float32 `json:"y"`
}

这个结构体可以存储矩形或圆形。然后,添加IsCircle和IsRectangle方法。

func (obj *Object) IsCircle() bool {
    return obj.Radius > 0
}

func (obj *Object) IsRectangle() bool {
    return obj.Base > 0 && obj.Height > 0
}

你可以根据自己的想法添加返回结构体标识的Kind()方法。最后,你应该添加ToCircle/ToRectangle方法。

func (obj *Object) ToCircle() *Circle {
    return &Circle{
        Radius: obj.Radius,
        X:      obj.X,
        Y:      obj.Y,
    }
}

func (obj *Object) ToRectangle() *Rectangle {
    return &Rectangle{
        Base:   obj.Base,
        Height: obj.Height,
        X:      obj.X,
        Y:      obj.Y,
    }
}

如果你想要Polygon接口的切片,你应该将这个Object的切片转换为Polygon的切片,如下所示。

var polygons []Polygon
for _, obj := range newSlice {
    if obj.IsCircle() {
        polygons = append(polygons, obj.ToCircle())
    } else if obj.IsRectangle() {
        polygons = append(polygons, obj.ToRectangle())
    }
}

另一种方法是创建一个从map[string]interface{}进行转换的转换器。转换器可以通过查找字段是否存在来检测结构体。

var converters = []func(map[string]interface{}) Polygon{
    func(m map[string]interface{}) Polygon {
        rectangle := new(Rectangle)
        if base, ok := m["base"]; ok {
            rectangle.Base = toFloat32(base)
        } else {
            return nil
        }
        if height, ok := m["height"]; ok {
            rectangle.Height = toFloat32(height)
        } else {
            return nil
        }
        if x, ok := m["x"]; ok {
            rectangle.X = toFloat32(x)
        }
        if y, ok := m["y"]; ok {
            rectangle.Y = toFloat32(y)
        }
        return rectangle
    },
    func(m map[string]interface{}) Polygon {
        circle := new(Circle)
        if radius, ok := m["radius"]; ok {
            circle.Radius = toFloat32(radius)
        } else {
            return nil
        }
        if x, ok := m["x"]; ok {
            circle.X = toFloat32(x)
        }
        if y, ok := m["y"]; ok {
            circle.Y = toFloat32(y)
        }
        return circle
    },
}

然后进行转换。

var polygons []Polygon
for _, obj := range newSlice {
    m, ok := obj.(map[string]interface{})
    if !ok {
        panic("invalid struct")
    }
    var p Polygon
    for _, converter := range converters {
        p = converter(m)
        if p != nil {
            break
        }
    }
    if p == nil {
        panic("unknown polygon")
    }
    polygons = append(polygons, p)
}

链接:https://play.golang.org/p/PrxiMOa_1F

英文:

Thought that way to distinguish whether the json is Circle or Rectangle. In your JSON, there is no identify for the struct which can detect kind of objects. So let's make rules.

  • Rectangle have base and height both greater than 0
  • Circle have radius greater than 0

To unmarshal JSON, it should have commonly fields like below.

type Object struct {
	Base   float32 `json:"base,omitempty"`
	Radius float32 `json:"radius,omitempty"`
	Height float32 `json:"height,omitempty"`
	X      float32 `json:"x"`
	Y      float32 `json:"y"`
}

This struct can be stored Rectangle or Circle both. Then, add method IsCircle and IsRectangle.

func (obj *Object) IsCircle() bool {
	return obj.Radius > 0
}

func (obj *Object) IsRectangle() bool {
	return obj.Base > 0 && obj.Height > 0
}

You can make method like Kind() to return identity of struct instead. As you think best. Finally, you should add ToCircle/ToRectangle methods.

func (obj *Object) ToCircle() *Circle {
	return &Circle{
		Radius: obj.Radius,
		X:      obj.X,
		Y:      obj.Y,
	}
}

func (obj *Object) ToRectangle() *Rectangle {
	return &Rectangle{
		Base:   obj.Base,
		Height: obj.Height,
		X:      obj.X,
		Y:      obj.Y,
	}
}

If you want slice of Polygon interface, you should convert this slice of Object to the slice of Polygon like below.

var polygons []Polygon
for _, obj := range newSlice {
	if obj.IsCircle() {
		polygons = append(polygons, obj.ToCircle())
	} else if obj.IsRectangle() {
		polygons = append(polygons, obj.ToRectangle())
	}
}

https://play.golang.org/p/kO_F4GTYdA

UPDATE

One another approach. Make converters which convert from map[string]interface{}. The converter can detect the struct with looking fields exists.

var converters = []func(map[string]interface{}) Polygon{
	func(m map[string]interface{}) Polygon {
		rectangle := new(Rectangle)
		if base, ok := m["base"]; ok {
			rectangle.Base = toFloat32(base)
		} else {
			return nil
		}
		if height, ok := m["height"]; ok {
			rectangle.Height = toFloat32(height)
		} else {
			return nil
		}
		if x, ok := m["x"]; ok {
			rectangle.X = toFloat32(x)
		}
		if y, ok := m["y"]; ok {
			rectangle.Y = toFloat32(y)
		}
		return rectangle
	},
	func(m map[string]interface{}) Polygon {
		circle := new(Circle)
		if radius, ok := m["radius"]; ok {
			circle.Radius = toFloat32(radius)
		} else {
			return nil
		}
		if x, ok := m["x"]; ok {
			circle.X = toFloat32(x)
		}
		if y, ok := m["y"]; ok {
			circle.Y = toFloat32(y)
		}
		return circle
	},
}

And do convert

var polygons []Polygon
for _, obj := range newSlice {
	m, ok := obj.(map[string]interface{})
	if !ok {
		panic("invalid struct")
	}
	var p Polygon
	for _, converter := range converters {
		p = converter(m)
		if p != nil {
			break
		}
	}
	if p == nil {
		panic("unknown polygon")
	}
	polygons = append(polygons, p)
}

https://play.golang.org/p/PrxiMOa_1F

huangapple
  • 本文由 发表于 2017年7月2日 21:33:14
  • 转载请务必保留本文链接:https://go.coder-hub.com/44871357.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定