【问题标题】:Custom UnmarshalYAML interface for an interface and its implementations用于接口及其实现的自定义 UnmarshalYAML 接口
【发布时间】:2020-03-11 13:02:02
【问题描述】:

我实现了一个接口 Fruit 和它的两个实现:AppleBanana

我想从 yaml 文件加载数据到两个实现的对象中:

capacity: 4
Apple:
- name: "apple1"
  number: 1
- name: "apple2"
  number: 1
Banana:
- name: "banana1"
  number: 2

我实现了UnmarshalYaml 接口来将数据加载到我的对象中:

package main

import (
    "errors"
    "gopkg.in/yaml.v3"
    "log"
    "fmt"
)

type FruitBasket struct {
    Capacity int `yaml:"capacity"`
    Fruits []Fruit
}

func NewFruitBasket() *FruitBasket {
    fb := new(FruitBasket)

    return fb
}

type Fruit interface {
    GetFruitName() string
    GetNumber() int
}

type Apple struct {
    Name string `yaml:"name"`
    Number int `yaml:"number"`
}

type Banana struct {
    Name string `yaml:"name"`
    Number int `yaml:"number"`
}

func (apple *Apple) GetFruitName() string {
    return apple.Name
}

func (apple *Apple) GetNumber() int {
    return apple.Number
}

func (banana *Banana) GetFruitName() string {
    return banana.Name
}

func (banana *Banana) GetNumber() int {
    return banana.Number
}

type tmpFruitBasket struct {
    Capacity int `yaml:"capacity"`
    Fruits []map[string]yaml.Node
}

func (fruitBasket *FruitBasket) UnmarshalYAML(value *yaml.Node) error {
    var tmpFruitBasket tmpFruitBasket

    if err := value.Decode(&tmpFruitBasket); err != nil {
        return err
    }

    fruitBasket.Capacity = tmpFruitBasket.Capacity

    fruits := make([]Fruit, 0, len(tmpFruitBasket.Fruits))

    for i := 0; i < len(tmpFruitBasket.Fruits); i++ {
        for tag, node := range tmpFruitBasket.Fruits[i] {
            switch tag {
            case "Apple":
                apple := &Apple{}
                if err := node.Decode(apple); err != nil {
                    return err
                }

                fruits = append(fruits, apple)
            case "Banana":
                banana := &Banana{}
                if err := node.Decode(banana); err != nil {
                    return err
                }

                fruits = append(fruits, banana)
            default:
                return errors.New("Failed to interpret the fruit of type: \"" + tag + "\"")
            }
        }
    }

    fruitBasket.Fruits = fruits

    return nil
}

func main() {
    data := []byte(`
capacity: 4
Apple:
- name: "apple1"
  number: 1
- name: "apple2"
  number: 1
Banana:
- name: "banana1"
  number: 2
`)

    fruitBasket := NewFruitBasket()

    err := yaml.Unmarshal(data, &fruitBasket)

    if err != nil {
        log.Fatalf("error: %v", err)
    }

    fmt.Println(fruitBasket.Capacity)

    for i := 0; i < len(fruitBasket.Fruits); i++ {
        switch fruit := fruitBasket.Fruits[i].(type) {
        case *Apple:
            fmt.Println(fruit.Name)
            fmt.Println(fruit.Number)
        }
    }
}

但是,这不起作用。 AppleBanana 标签的数据似乎没有加载。可能是因为我的 tmpFruitBasket 结构中缺少 Fruits 切片的 yaml 标志。但是,由于 Fruit 是一个接口,我无法定义 yaml 标志。将来,我想实现代表具体水果(例如草莓)的其他结构,实现接口Fruit

你知道如何解决这个问题吗?

【问题讨论】:

  • 您的结构与您的 YML 完全不匹配。您的 YML 定义了一个包含三个字段的对象:capacity(一个 int)、Apple(一个对象列表)和 Banana(一个对象列表)。同样值得注意的是,Go 中没有“抽象结构”之类的东西。你有一个接口

标签: go struct yaml unmarshalling


【解决方案1】:

这是你需要的中间类型:

type tmpFruitBasket struct {
  Capacity int
  Apple    []yaml.Node `yaml:"Apple"`
  Banana   []yaml.Node `yaml:"Banana"`
}

然后,加载函数将如下所示:

// helper to load a list of nodes as a concrete type
func appendFruits(fruits []Fruit, kind reflect.Type, input []yaml.Node) ([]Fruit, error) {
  for i := range input {
    val := reflect.New(kind).Interface()
    if err := input[i].Decode(val); err != nil {
      return nil, err
    }
    fruits = append(fruits, val.(Fruit))
  }
  return fruits, nil
}


func (fruitBasket *FruitBasket) UnmarshalYAML(value *yaml.Node) error {
    var tmp tmpFruitBasket

    if err := value.Decode(&tmp); err != nil {
        return err
    }

    fruitBasket.Capacity = tmp.Capacity

    var fruits []Fruit
    var err error
    // sadly, there is no nicer way to get the reflect.Type of Apple / Banana
    fruits, err = appendFruits(
      fruits, reflect.TypeOf((*Apple)(nil)).Elem(), tmp.Apple)
    if err != nil {
      return err
    }
    fruits, err = appendFruits(
      fruits, reflect.TypeOf((*Banana)(nil)).Elem(), tmp.Banana)
    if err != nil {
      return err
    }

    fruitBasket.Fruits = fruits
    return nil
}

编辑:如果您坚持将每种类型分类到一个专用切片中,您当然可以直接将它们键入为[]Apple[]Banana,然后将它们合并。这个答案是对动态加载输入到不同类型的问题的继续,从您之前的问题开始。仅当您在某些时候不再知道静态类型时(例如,如果您提供 API 以在运行时添加其他水果类型),这样做才有意义。

【讨论】:

  • 谢谢!效果很好! go-yaml 是否允许为变量指定多个替代 yaml 标签?如果是这样,中间结构必须只包含Fruit接口,而不是ApplesBananas
  • 请不要称它们为标签,它们是。标签是以! 开头的标记,您目前没有使用它们。通过从yaml.Nodes 构造本机值,您几乎可以做任何事情(但您需要自己通过解释节点中的值来实现它)。对于你的每一个问题,我越来越觉得这一切都是精心设计的XY problem,你应该写下你实际上想要做的事情,而不是在每个答案之后都问“好吧但是如果我想以不同的方式做呢?“。
  • 我只是想知道是否会有另一种解决方案来解决 go-yaml 从头开始​​支持的问题。无论如何,您的回答是解决我的问题的方法。所以,非常感谢您的帮助!
  • 除了inline,go-yaml 没有任何开箱即用的结构改变功能。
猜你喜欢
  • 1970-01-01
  • 2018-09-06
  • 1970-01-01
  • 2016-09-12
  • 1970-01-01
  • 2017-07-09
  • 2018-07-17
  • 1970-01-01
  • 2020-11-05
相关资源
最近更新 更多