为什么在golang中,YAML.v3不根据String()方法来序列化结构体?

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

Why doesn't YAML.v3 marshal a struct based on the String() in golang?

问题

我有一个包含基于枚举的类型的结构体。我想将其渲染为用户友好的字符串。以下是最小可行代码:

package main

import (
	"fmt"

	"gopkg.in/yaml.v3"
)

type Job struct {
	Engine Engine `json:"Engine" yaml:"Engine"`
}

//go:generate stringer -type=Engine --trimprefix=Engine
type Engine int

const (
	engineUnknown Engine = iota // 必须是第一个
	EngineDocker
	engineDone // 必须是最后一个
)

func main() {
	j := Job{Engine: EngineDocker}

	fmt.Printf("%+v\n\n", j)
	out, _ := yaml.Marshal(j)
	fmt.Println(string(out))
}

这是生成的代码:

// Code generated by "stringer -type=Engine --trimprefix=Engine"; DO NOT EDIT.

package main

import "strconv"

func _() {
	// "invalid array index" 编译错误表示常量值已更改。
	// 重新运行 stringer 命令以再次生成它们。
	var x [1]struct{}
	_ = x[engineUnknown-0]
	_ = x[EngineDocker-1]
	_ = x[engineDone-2]
}

const _Engine_name = "engineUnknownDockerengineDone"

var _Engine_index = [...]uint8{0, 13, 19, 29}

func (i Engine) String() string {
	if i < 0 || i >= Engine(len(_Engine_index)-1) {
		return "Engine(" + strconv.FormatInt(int64(i), 10) + ")"
	}
	return _Engine_name[_Engine_index[i]:_Engine_index[i+1]]
}

这是输出:

{Engine:1}

Engine: 1

这是我希望输出的内容:

{Engine:Docker}

Engine: Docker

我以为生成的文件中的String()方法可以实现这个。有没有办法做到这一点?谢谢!

英文:

I have a struct which contains a type based on an enum. I am trying to render it to a user friendly string. Here's minimum viable code:

package main

import (
	&quot;fmt&quot;

	&quot;gopkg.in/yaml.v3&quot;
)

type Job struct {
	Engine Engine `json:&quot;Engine&quot; yaml:&quot;Engine&quot;`
}

//go:generate stringer -type=Engine --trimprefix=Engine
type Engine int

const (
	engineUnknown Engine = iota // must be first
	EngineDocker
	engineDone // must be last
)

func main() {
	j := Job{Engine: EngineDocker}

	fmt.Printf(&quot;%+v\n\n&quot;, j)
	out, _ := yaml.Marshal(j)
	fmt.Println(string(out))
}

Here's the generated code:

// Code generated by &quot;stringer -type=Engine --trimprefix=Engine&quot;; DO NOT EDIT.

package main

import &quot;strconv&quot;

func _() {
	// An &quot;invalid array index&quot; compiler error signifies that the constant values have changed.
	// Re-run the stringer command to generate them again.
	var x [1]struct{}
	_ = x[engineUnknown-0]
	_ = x[EngineDocker-1]
	_ = x[engineDone-2]
}

const _Engine_name = &quot;engineUnknownDockerengineDone&quot;

var _Engine_index = [...]uint8{0, 13, 19, 29}

func (i Engine) String() string {
	if i &lt; 0 || i &gt;= Engine(len(_Engine_index)-1) {
		return &quot;Engine(&quot; + strconv.FormatInt(int64(i), 10) + &quot;)&quot;
	}
	return _Engine_name[_Engine_index[i]:_Engine_index[i+1]]
}

Here's the output:

{Engine:1}

Engine: 1

Here's what I'd like the output to be:

{Engine:Docker}

Engine: Docker

I thought the String() in the generated file would accomplish this. Is there any way to do this? Thanks!

答案1

得分: 1

yaml编组器不使用String方法。相反,YAML使用encoding.TextMarshalerencoding.TextUnmarshaler接口。实际上,所有其他编解码方案 - JSON、XML、TOML等 - 都使用这些接口来读取/写入值。因此,如果您为自己的类型实现这些方法,您将免费获得所有其他编解码器。

这是一个示例,演示如何为枚举类型创建可读性强的编码:https://go.dev/play/p/pEcBmAM-oZJ

type Engine int

const (
	engineUnknown Engine = iota // 必须是第一个
	EngineDocker
	engineDone // 必须是最后一个
)

var engineNames []string
var engineNameToValue map[string]Engine

func init() {
	engineNames = []string{"Unknown", "Docker"}
	engineNameToValue = make(map[string]Engine)
	for i, name := range engineNames {
		engineNameToValue[strings.ToLower(name)] = Engine(i)
	}
}

func (e Engine) String() string {
	if e < 0 || int(e) >= len(engineNames) {
		panic(fmt.Errorf("Invalid engine code: %d", e))
	}
	return engineNames[e]
}

func ParseEngine(text string) (Engine, error) {
	i, ok := engineNameToValue[strings.ToLower(text)]
	if !ok {
		return engineUnknown, fmt.Errorf("Invalid engine name: %s", text)
	}
	return i, nil
}

func (e Engine) MarshalText() ([]byte, error) {
	return []byte(e.String()), nil
}

func (e *Engine) UnmarshalText(text []byte) (err error) {
	name := string(text)
	*e, err = ParseEngine(name)
	return
}

它的工作原理:

func main() {
	j := Job{Engine: EngineDocker}

	fmt.Printf("%#v\n\n", j)
	out, err := yaml.Marshal(j)
	if err != nil {
		panic(err)
	}
	fmt.Printf("YAML: %s\n", string(out))

	var jj Job
	err = yaml.Unmarshal(out, &jj)
	if err != nil {
		panic(err)
	}
	fmt.Printf("%#v\n\n", jj)

	// == JSON ==
	out, err = json.Marshal(j)
	if err != nil {
		panic(err)
	}
	fmt.Printf("JSON: %s\n", string(out))

	var jjs Job
	err = json.Unmarshal(out, &jjs)
	if err != nil {
		panic(err)
	}
	fmt.Printf("%#v\n\n", jjs)

}

输出结果:

main.Job{Engine:1}

YAML: Engine: Docker

main.Job{Engine:1}

JSON: {"Engine":"Docker"}
main.Job{Engine:1}

看到了吗?它在YAML和JSON中都以字符串形式写入和读取,而无需任何额外的努力。

英文:

yaml marshaler doesn't use String method. Instead YAML uses encoding.TextMarshaler and encoding.TextUnmarshaler interfaces. Actually, all other codec schemes - JSON, XML, TOML, etc. - use those interfaces to read/write the values. So, if you implement those methods for your type, you will receive all other codecs for free.

Here is an example how to make a human-readable encoding for your enum: https://go.dev/play/p/pEcBmAM-oZJ

type Engine int

const (
	engineUnknown Engine = iota // must be first
	EngineDocker
	engineDone // must be last
)

var engineNames []string
var engineNameToValue map[string]Engine

func init() {
	engineNames = []string{&quot;Unknown&quot;, &quot;Docker&quot;}
	engineNameToValue = make(map[string]Engine)
	for i, name := range engineNames {
		engineNameToValue[strings.ToLower(name)] = Engine(i)
	}
}

func (e Engine) String() string {
	if e &lt; 0 || int(e) &gt;= len(engineNames) {
		panic(fmt.Errorf(&quot;Invalid engine code: %d&quot;, e))
	}
	return engineNames[e]
}

func ParseEngine(text string) (Engine, error) {
	i, ok := engineNameToValue[strings.ToLower(text)]
	if !ok {
		return engineUnknown, fmt.Errorf(&quot;Invalid engine name: %s&quot;, text)
	}
	return i, nil
}

func (e Engine) MarshalText() ([]byte, error) {
	return []byte(e.String()), nil
}

func (e *Engine) UnmarshalText(text []byte) (err error) {
	name := string(text)
	*e, err = ParseEngine(name)
	return
}

How it works:

func main() {
	j := Job{Engine: EngineDocker}

	fmt.Printf(&quot;%#v\n\n&quot;, j)
	out, err := yaml.Marshal(j)
	if err != nil {
		panic(err)
	}
	fmt.Printf(&quot;YAML: %s\n&quot;, string(out))

	var jj Job
	err = yaml.Unmarshal(out, &amp;jj)
	if err != nil {
		panic(err)
	}
	fmt.Printf(&quot;%#v\n\n&quot;, jj)

	// == JSON ==
	out, err = json.Marshal(j)
	if err != nil {
		panic(err)
	}
	fmt.Printf(&quot;JSON: %s\n&quot;, string(out))

	var jjs Job
	err = json.Unmarshal(out, &amp;jjs)
	if err != nil {
		panic(err)
	}
	fmt.Printf(&quot;%#v\n\n&quot;, jjs)

}

the output

main.Job{Engine:1}

YAML: Engine: Docker

main.Job{Engine:1}

JSON: {&quot;Engine&quot;:&quot;Docker&quot;}
main.Job{Engine:1}

See? It writes and reads strings to both YAML and JSON without any extra effort.

huangapple
  • 本文由 发表于 2022年9月28日 08:31:46
  • 转载请务必保留本文链接:https://go.coder-hub.com/73875048.html
匿名

发表评论

匿名网友

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

确定