将强制解组为interface{}而不是map[string]interface{}。

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

Force unmarshal as interface{} instead of map[string]interface{}

问题

我有以下的YAML结构:

type Pipeline struct {
    Name        string                  `yaml:"name"`
    Nodes       map[string]NodeConfig   `yaml:"nodes"`
    Connections []NodeConnection        `yaml:"connections"`
}

type NodeConfig struct {
    Type   string      `yaml:"type"`
    Config interface{} `yaml:"config"`
}

对于每个NodeConfig,根据Type的值,我需要检测Config的真实类型。

switch nc.Type {
    case "request":
        return NewRequestNode(net, name, nc.Config.(RequestConfig))
    case "log":
        return NewLogNode(net, name)
    //...
}

我得到了以下错误:

panic: interface conversion: interface {} is map[string]interface {}, not main.RequestConfig

我怀疑这是因为Config被自动识别为map[string]interface{},而我实际上希望它只是一个interface{}。我该如何解决这个问题?

编辑:最小示例

英文:

I have the following YAML structure:

type Pipeline struct {
    Name string                  `yaml:"name"`
    Nodes map[string]NodeConfig  `yaml:"nodes"`
    Connections []NodeConnection `yaml:"connections"`
}

type NodeConfig struct {
    Type   string      `yaml:"type"`
    Config interface{} `yaml:"config"`
}

For each NodeConfig, depending on the value of Type, I need to detect the real type of Config.

switch nc.Type {
    case "request":
        return NewRequestNode(net, name, nc.Config.(RequestConfig))
    case "log":
        return NewLogNode(net, name)
    //...
}

This is the error I get from this:

panic: interface conversion: interface {} is map[string]interface {}, not main.RequestConfig

I suspect it's because Config is getting automatically recognized as a map[string]interface{}, when I really want it to just be an interface{}. How can I do this?

Edit: Minimal Example

答案1

得分: 1

你对问题的理解是正确的,它被自动识别为map[string]interface{},因为你没有提供自定义的UnmarshalYAML函数,YAML包只能这样处理。但实际上你不希望它只是interface{},你需要确定你想要的实际实现。

使用yaml.v3的解决方案

我认为你无法在不提供自定义的UnmarshalYAML函数给NodeConfig类型的情况下解决这个问题。如果是JSON,我会将Config读取为json.RawMessage,然后对每种可能的类型进行解组,而yaml.v3的等效方式似乎是使用yaml.Node类型

使用这种方式,你可以创建一个类似于NodeConfig的结构体,其中Configyaml.Node类型,并根据Type值将其转换为具体类型,像这样:

func (nc *NodeConfig) UnmarshalYAML(value *yaml.Node) error {
	var ncu struct {
		Type   string    `yaml:"type"`
		Config yaml.Node `yaml:"config"`
	}
	var err error

	// 解组为NodeConfigUnmarshaler以检测正确的类型
	err = value.Decode(&ncu)
	if err != nil {
		return err
	}

	// 现在,根据类型进行转换
	nc.Type = ncu.Type
	switch ncu.Type {
	case "request":
		nc.Config = &RequestConfig{}
	case "log":
		nc.Config = &LogConfig{}
	default:
		return fmt.Errorf("unknown type %q", ncu.Type)
	}
	err = ncu.Config.Decode(nc.Config)

	return err
}

示例代码

为了测试这个,我创建了RequestConfigLogConfig的虚拟结构体以及一个示例:

type RequestConfig struct {
	Foo string `yaml:"foo"`
	Bar string `yaml:"bar"`
}

type LogConfig struct {
	Message string `yaml:"message"`
}

func main() {
	logSampleYAML := []byte(`
type: log
config:
    message: this is a log message
`)

	reqSampleYAML := []byte(`
type: request
config:
    foo: foo value
    bar: bar value
`)

	for i, val := range [][]byte{logSampleYAML, reqSampleYAML} {
		var nc NodeConfig
		err := yaml.Unmarshal(val, &nc)
		if err != nil {
			fmt.Printf("failed to parse sample %d: %v\n", i, err)
		} else {
			fmt.Printf("sample %d type %q (%T) = %+v\n", i, nc.Type, nc.Config, nc.Config)
		}
	}
}

输出结果为:

sample 0 type "log" (*main.LogConfig) = &{Message:this is a log message}
sample 1 type "request" (*main.RequestConfig) = &{Foo:foo value Bar:bar value}

因此,你可以看到每个NodeConfig实例都使用所需的具体类型实例化了Config,这意味着你现在可以使用类型断言,如Config.(*RequestConfig)Config.(*LogConfig)(或使用switch语句,当然也可以)。

你可以在这个Go Playground完整示例中尝试这个解决方案

使用yaml.v2的解决方案

我犯了一个错误,并发送了一个使用v2的解决方案,但我建议任何人使用v3。如果你不能使用v3,请按照v2版本的解决方案...

v2没有yaml.Node,但我在这个问题的答案中找到了一个非常相似的解决方案(我在那里修复了一个拼写错误):

type RawMessage struct {
	unmarshal func(interface{}) error
}

func (msg *RawMessage) UnmarshalYAML(unmarshal func(interface{}) error) error {
	msg.unmarshal = unmarshal
	return nil
}

func (msg *RawMessage) Unmarshal(v interface{}) error {
	return msg.unmarshal(v)
}

这是一个有趣的技巧,通过将其加载到临时结构体中,然后识别每个你想要的类型,而无需处理两次YAML,你可以自己烘烤UnmarshalYAML函数:

func (nc *NodeConfig) UnmarshalYAML(unmarshal func(interface{}) error) error {
	var ncu struct {
		Type   string     `yaml:"type"`
		Config RawMessage `yaml:"config"`
	}
	var err error

	// 解组为NodeConfigUnmarshaler以检测正确的类型
	err = unmarshal(&ncu)
	if err != nil {
		return err
	}

	// 现在,根据类型进行转换
	nc.Type = ncu.Type
	switch ncu.Type {
	case "request":
		cfg := &RequestConfig{}
		err = ncu.Config.Unmarshal(cfg)
		nc.Config = cfg
	case "log":
		cfg := &LogConfig{}
		err = ncu.Config.Unmarshal(cfg)
		nc.Config = cfg
	default:
		return fmt.Errorf("unknown type %q", ncu.Type)
	}

	return err
}

v2和v3的示例代码是相同的。

你可以在这个Go Playground完整示例中尝试这个解决方案

英文:

You are correct about the problem, it is getting automatically recognized as a map[string]interface{}, since you don't provide a custom UnmarshalYAML func the YAML package can only do that. But you actually don't want it as just interface{}, you need to identify which actual implementation you want for that.

Solution using yaml.v3

I don't see how you can solve it without providing a custom UnmarshalYAML func to NodeConfig type. If that was JSON, I would read the Config as a json.RawMessage, then for each possible type I would unmarshal it into the desired type, and yaml.v3 equivalent seems to be a yaml.Node type.

Using this, you can create a struct similar to NodeConfig which has the Config as yaml.Node and convert it to the concrete type based on the Type value, like this:

func (nc *NodeConfig) UnmarshalYAML(value *yaml.Node) error {
	var ncu struct {
		Type   string    `yaml:"type"`
		Config yaml.Node `yaml:"config"`
	}
	var err error

	// unmarshall into a NodeConfigUnmarshaler to detect correct type
	err = value.Decode(&ncu)
	if err != nil {
		return err
	}

	// now, detect the type and covert it accordingly
	nc.Type = ncu.Type
	switch ncu.Type {
	case "request":
		nc.Config = &RequestConfig{}
	case "log":
		nc.Config = &LogConfig{}
	default:
		return fmt.Errorf("unknown type %q", ncu.Type)
	}
	err = ncu.Config.Decode(nc.Config)

	return err
}

Sample code

To test that, I created dummies RequestConfig and LogConfig and a sample:

type RequestConfig struct {
	Foo string `yaml:"foo"`
	Bar string `yaml:"bar"`
}

type LogConfig struct {
	Message string `yaml:"message"`
}

func main() {
	logSampleYAML := []byte(`
type: log
config:
    message: this is a log message
`)

	reqSampleYAML := []byte(`
type: request
config:
    foo: foo value
    bar: bar value
`)

	for i, val := range [][]byte{logSampleYAML, reqSampleYAML} {
		var nc NodeConfig
		err := yaml.Unmarshal(val, &nc)
		if err != nil {
			fmt.Printf("failed to parse sample %d: %v\n", i, err)
		} else {
			fmt.Printf("sample %d type %q (%T) = %+v\n", i, nc.Type, nc.Config, nc.Config)
		}
	}
}

Which outputs:

sample 0 type "log" (*main.LogConfig) = &{Message:this is a log message}
sample 1 type "request" (*main.RequestConfig) = &{Foo:foo value Bar:bar value}

So, as you can see each instance of NodeConfig is instanciating the Config with the concrete type required, which means you can now use type assertion as Confg.(*RequestConfig) or Config.(*LogConfig) (or switch, of course).

You can play with that solution in this Go Playground full sample.

Solution using yaml.v2

I have made a mistake and sent a solution with v2, but I recommend anyone to use the v3. If you can't, follow the v2 version...

The v2 does not have yaml.Node, but I found a very similar solution in the answer of this issue (I fixed a typo there):

type RawMessage struct {
	unmarshal func(interface{}) error
}

func (msg *RawMessage) UnmarshalYAML(unmarshal func(interface{}) error) error {
	msg.unmarshal = unmarshal
	return nil
}

func (msg *RawMessage) Unmarshal(v interface{}) error {
	return msg.unmarshal(v)
}

Which is an interesting trick, and with that you could bake your own UnmarshalYAML func by loading it into a temporary struct and then identifying each type you want and without needing to process the YAML twice:

func (nc *NodeConfig) UnmarshalYAML(unmarshal func(interface{}) error) error {
	var ncu struct {
		Type   string     `yaml:"type"`
		Config RawMessage `yaml:"config"`
	}
	var err error

	// unmarshall into a NodeConfigUnmarshaler to detect correct type
	err = unmarshal(&ncu)
	if err != nil {
		return err
	}

	// now, detect the type and covert it accordingly
	nc.Type = ncu.Type
	switch ncu.Type {
	case "request":
		cfg := &RequestConfig{}
		err = ncu.Config.Unmarshal(cfg)
		nc.Config = cfg
	case "log":
		cfg := &LogConfig{}
		err = ncu.Config.Unmarshal(cfg)
		nc.Config = cfg
	default:
		return fmt.Errorf("unknown type %q", ncu.Type)
	}

	return err
}

The sample code for v2 and v3 are identical.

You can play with that solution in this Go Playground full sample.

huangapple
  • 本文由 发表于 2022年10月6日 03:56:24
  • 转载请务必保留本文链接:https://go.coder-hub.com/73965697.html
匿名

发表评论

匿名网友

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

确定