接口及其实现的自定义 UnmarshalYAML 接口 [英] Custom UnmarshalYAML interface for an interface and its implementations
问题描述
我实现了一个接口 Fruit
和它的两个实现:Apple
和 Banana
.
I implemented an interface Fruit
and two implementations of it: Apple
and Banana
.
到两个实现的对象中我想从 yaml 文件加载数据:
Into objects of the two implementations I want to load data from a yaml file:
capacity: 4
Apple:
- name: "apple1"
number: 1
- name: "apple2"
number: 1
Banana:
- name: "banana1"
number: 2
我实现了 UnmarshalYaml
接口以将数据加载到我的对象中:
I implemented the UnmarshalYaml
interface to load data into my objects:
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)
}
}
}
然而,这是行不通的.似乎没有加载 Apple
和 Banana
标签的数据.可能是因为在我的 tmpFruitBasket
结构中缺少 Fruits
切片的 yaml 标志.但是,由于 Fruit
是一个接口,我无法定义 yaml 标志.将来,我想实现其他表示具体水果(例如草莓)的结构,实现接口 Fruit
.
However, this is not working. It seems that the data for the Apple
and Banana
tags are not loaded. Probably, because of the missing yaml flag for the Fruits
slice in my tmpFruitBasket
struct. But, as Fruit
is an interface, I cannot define a yaml flag. In the future, I want to implement other structs representing concrete fruits (e.g., Strawberry) implementing the interface Fruit
.
知道如何解决这个问题吗?
Any idea on how to solve this?
推荐答案
这是您需要的中间类型:
This is the intermediate type you need:
type tmpFruitBasket struct {
Capacity int
Apple []yaml.Node `yaml:"Apple"`
Banana []yaml.Node `yaml:"Banana"`
}
然后,加载函数将如下所示:
Then, the loading function will look like:
// 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 以在运行时添加其他水果类型).
If you stick with sorting each type into a dedicated slice, you could of course directly type those as []Apple
and []Banana
and just merge them. This answer is a continuation of digging into the issue of dynamically loading input to different types, started with your previous questions. Doing this only makes sense if you at some point don't know the static type anymore (e.g. if you provide an API to add additional fruit types at runtime).
这篇关于接口及其实现的自定义 UnmarshalYAML 接口的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!