英文:
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 (
	"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 // must be first
	EngineDocker
	engineDone // must be last
)
func main() {
	j := Job{Engine: EngineDocker}
	fmt.Printf("%+v\n\n", j)
	out, _ := yaml.Marshal(j)
	fmt.Println(string(out))
}
Here's the generated code:
// Code generated by "stringer -type=Engine --trimprefix=Engine"; DO NOT EDIT.
package main
import "strconv"
func _() {
	// An "invalid array index" 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 = "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]]
}
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.TextMarshaler和encoding.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{"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
}
How it works:
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)
}
the output
main.Job{Engine:1}
YAML: Engine: Docker
main.Job{Engine:1}
JSON: {"Engine":"Docker"}
main.Job{Engine:1}
See? It writes and reads strings to both YAML and JSON without any extra effort.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。


评论