《Shell 转 Go》
Toggle Dark/Light/Auto mode Toggle Dark/Light/Auto mode Toggle Dark/Light/Auto mode Back to homepage

2. 配置文件读取与写入

作业要求

  1. 使用 https://github.com/spf13/cobra 实现命令工具
  2. 命令具有以下参数
    1. --config , -c 配置文件

配置文件如下

# config.yml
name: zhangsan
age: 20
  1. 将配置文件保存为 JSON 格式
$ cat config.json

输出结果

{
    "name":"zhangsan",
    "age": 20
}

单个参数绑定的困境

之前我们使用了 单个参数独立绑定 的方式, 为我们的 greeting 应用绑定了 name 和 age 参数。

这种方式有个很明显的缺点, 如果应用参数的数量较多(比如说几十个) 的时候, 就会出现庞大的参数列表, 可读性 和 维护性 都会变得很差。

var (
	name string
	age  int
)

func init() {
	root.Flags().StringVarP(&name, "name", "n", "", "名字")
	root.Flags().IntVarP(&age, "age", "a", 20, "名字")
}

因此在生产中, 我们常常会选择 配置 文件来进行 参数/变量 的管理。 例如作业要求中提到的 YAML 或者 JSON 文件。

配置文件

最常用 的的配置文件类型就是 YAML 和 JSON。 除此之外还有 toml, xml, ini 等, 这些类型的配置文件可以在实际碰到的时候去学习掌握, 大差不差。

通常 解析 和 映射 方法名称分别为 Marshal 和 Unmarshal

  1. Marshal(v any) ([]byte, error) 将结构体 解析 成 []byte 类型。
  2. Unmarshal(data []byte, v any) error 将 []byte 映射 到结构体中。 这里的 v 需要是 指针类型

但有些库在名字上可能具有一定变种,通常也有迹可循。

MarshalJSON
MarshalYAMl

不同库 对应的的 方法名称 和 实现逻辑 也不尽相同, 如果凭经验使用遇到了问题, 一定要去看看官网文档。

最基本常用的解析库

  1. json: encoding/json
  2. yaml: gopkg.in/yaml.v3, gopkg.in/yaml.v2 v2 和 v3 有区别, 自己研究。

解题过程

1. 指定配置文件参数

由于我们的实际 参数/变量 通过配置文件管理了, 因此我们就需要指定一个配置文件参数。

var config string

func init() {
	root.Flags().StringVarP(&config, "config", "c", "config.yml", "配置文件")
}

有了上一篇作业的经验, 这个应该就很简单。

2. 读取配置

我们定义了一个名为 Person 的结构体, 包含两个字段 Name 和 Age, 用于接受参数/变量。

type Person struct {
	Name string `yaml:"name,omitempty"`
	Age  int    `yaml:"age,omitempty"`
}

我们在字段后面, 使用 tag 进行了信息补充, 提供 yaml 解析的行为参数。

golang-struct-tag

图片是之前 struct 的截图, 其中

  1. (1) 是字段名称
  2. (2-4) 是 tag 内容。
    1. (2) 是 tag name, 解析库依赖此字段进行判定。
    2. (3-4) 是 tag value, 本质上是 字符串。 例如这里 name 和 omitempty 使用 分隔符, 进行分割。 不同的库的分隔符有所不同, 例如 gorm 使用的 分号;。
    3. (3) 在这里对应的是 映射 的字段名称。 例如 yaml 文件中的 name 对应结构体中的 Name。 这里 name 也可以是其他值, 例如 MyName, 那么就会在 yaml文件中找对应的 MyName 字段。
    4. (4) omitempty 是 yaml 支持的操作符, 同时还支持 flow, inline。 具体功能描述可以参考 https://pkg.go.dev/gopkg.in/yaml.v3#pkg-functions
func readConfig(name string) *Person {
	person := &Person{}

	// 1. 读取文件
	b, err := os.ReadFile(config)
	if err != nil {
		panic(err)
	}

	// 2. 绑定参数
	err2 := yaml.Unmarshal(b, person)
	if err2 != nil {
		panic(err)
	}

	return person
}
  1. 初始化 person 指针对象, 其底层类型为 Person 结构体。
  2. 在 readConfig 函数中, 我们使用 os.ReadFile 读取文件内容。
  3. 并通过 yaml.Unmarshal 将数据映射到 person 实例中。 需要 注意的是, person 所在的参数未知, 必须是 指针 对象, 否则反射无法保存数据到 内存 中。

保存文件为 json

把配置信息保存为 json 文件, 就是之前 yaml 的反操作。

// dumpConfig 保存文件
func dumpConfig(person *Person) {
	// 将结构体解析成 []byte
	b, err := json.Marshal(person)
	if err != nil {
		panic(err)
	}

	// os.ModePerm => folder 755, file 644
	err2 := os.WriteFile("config.json", b, os.ModePerm)
	if err2 != nil {
		panic(err)
	}
}
  1. 使用 json.Marshal 将对象转换成 []byte。 由于是 读取操作, person 所在的参数为止可以是结构体, 也可以是指针。
  2. 使用 os.WriteFile 进行文件写入操作。 需要注意的是 os.ModePerm 这个权限, 对于文件夹是 755, 对于文件是 644, 非常的灵活。
{"Name":"zhugeliang","Age":2600}

执行后,得到的结果与期望的结果有一点点不同。注意 ,这里的 Name 和 Age 都是大写。

我们对 Person 结构体稍微做一点改造

type Person struct {
	Name string `yaml:"name,omitempty" json:"name,omitempty"`
	Age  int    `yaml:"age,omitempty" json:"age,omitempty"`
}

加上 json tag 之后, 得到的结果就与期望一致了。

{"name":"zhugeliang","age":2600}

思考题

留一点思考题吧。

  1. 为什么没有设置 json tag 也可以成功保存 json 配置?
  2. 设置了 json tag 之后, json 配置中的字段名字变了, 他们的优先级是什么?
  3. 要怎么 忽略 一个字段?

答案在官方文档中。