【问题标题】:Golang Unmarshal slice of type interfaceGolang Unmarshal 类型接口切片
【发布时间】:2017-07-02 13:33:14
【问题描述】:

在此示例中,我将尝试加载包含多边形的 2D 场景。在代码中,我会有许多不同的结构,例如圆形、方形、矩形、五边形等。 所有人都将共享共同的功能,例如面积和周长。 场景本身将存储为 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 个多边形组成的切片,对其进行编组,然后再次尝试对其进行解组。 编组后的字符串是:

[{"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

如果这可行,它将非常有用且易于使用。我会说Unmarshall 无法从json字符串中区分RectangleCircle,因此它不可能知道要构建什么结构。

有没有办法标记结构或告诉Unmarshal如何区分这个结构?

【问题讨论】:

  • Polygon 是一个接口,你不能将interface 传递给解组器。您必须传递 具体 类型。 快速 解决方案是将其解组为[]map[string]interface{},但随后您需要将其转换 以手动构造。如果允许更改数据结构,请使用json.RawMessage to delay parsing part of your JSON

标签: json go struct interface unmarshalling


【解决方案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"`
}

这个结构可以同时存储 Rectangle 或 Circle 。然后,添加方法 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,
    }
}

如果你想要多边形界面的切片,你应该将这个对象切片转换为多边形的切片,如下所示。

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

更新

另一种方法。制作从 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

【讨论】:

  • 我想避免这种形式的解决方案,因为拥有一个可以保存所有可能值的结构可能会降低效率。此外,它看起来并不那么漂亮(在我看来)。不过它应该可以工作,因为多边形没有那么多不同类型的值,所以还不错。
  • 添加了另一种方法。
  • 取决于半径 = 0 的矩形,这样的条件对我来说仍然不是很吸引人。到那时,我会发现更容易指定正在定义的结构的类型并手动解析映射。
  • 第二种方法看起来不像radius = 0。所以我猜你想要第二个。
  • 检查该值是否存在几乎是相同的。在这个例子中它可能很好,但一般来说,一些“多边形”(如通用类型)可以共享其他元素。添加新的会改变整个转换函数,使它们更难维护。
猜你喜欢
  • 2018-06-18
  • 2012-09-27
  • 2019-07-21
  • 1970-01-01
  • 2016-05-25
  • 2013-10-23
  • 2017-08-29
  • 2014-08-08
  • 1970-01-01
相关资源
最近更新 更多