「这是我参与11月更文挑战的第1天,活动详情查看:2021最后一次更文挑战」。
前言:
个人在开发小工具时读取 yaml 文件转 stuct 时定义的一个公用函数。
明明很简单的一个函数,因为自己对 golang 的接口理解不够,以至于走了很多弯路。
所以在此简单记录下。
golang 读取 yaml 文件转 struct 公用函数
- 网上能查到很多相关的方法,但在搜索过程中,发现都是和其它代码写在一起的,没有定义成单独的函数。
- 但是网上查到的大多数方法,不同结构的 yaml 转换成 不同的struct时,定义成不同的函数,结果定义了一堆相似的函数。于是一通抱怨 golang 没有泛型。
- 最后发现不同结构的 yaml 转换成 不同的struct时,可以使用接口定义成一个公用函数。明明是自己菜,对接口的理解不足。
-
网上能搜索到的方法大致如下。
使用的 yaml 包是 gopkg.in/yaml.v3package main import ( "gopkg.in/yaml.v3" "io/ioutil" "log" ) // Person 人 type Person struct { Name string `yaml:"name"` Sex string `yaml:"sex"` Age int `yaml:"age"` } func main() { file := "family.yaml" // 读取文件 b, err := ioutil.ReadFile(file) if err != nil { log.Print(err) return } var person Person // 转换成Struct err = yaml.Unmarshal(b, &person) if err != nil { log.Printf("%v\n", err.Error()) } }
Yaml 文件内容
person.yamlname: panda sex: M age: 22
-
个人做的小项目中,经常有多个地方需要读取 yaml 文件转成不同的struct,使用上面的方法,总觉着有些繁琐。
鉴于个人智商所限,居然很长时间没有想出来怎么定义成公用的函数。
只根据各个不同的struct,定义多个读取的方法了。比如读取上面的 person.yaml 的,像下面这样定义个 ReadPerson 函数。
参数为要读取的目标 yaml 文件。package main import ( "fmt" "gopkg.in/yaml.v3" "io/ioutil" "log" ) // Person 人 type Person struct { Name string `yaml:"name"` Sex string `yaml:"sex"` Age int `yaml:"age"` } func main() { file := "person.yaml" person := ReadPerson(file) fmt.Println(person) } // ReadPerson 读取 Yaml 文件转成 Person func ReadPerson(file string) (person Person){ // 读取文件 b, err := ioutil.ReadFile(file) if err != nil { log.Print(err) return person } // 转换成Struct err = yaml.Unmarshal(b, &person) if err != nil { log.Printf("%v\n", err.Error()) } return person }
多个不同内容的 yaml 文件,就定义多个 ReadXxx 函数。
然后就出来一坨 ReadXxx 函数。
先是想着,等 golang 泛型出来于提成公用函数吧。最后一通抱怨 golang 没有泛型。
后来也不知道怎么开窍了,原来把参数改成接口就可以了。
-
定义成公用函数。
函数有两个参数,一个是要读取的目标 yaml 文件,一个是要转换成的目标struct的变量的地址。
原来对接口还是理解得太浅(要被同行们笑掉大牙了)。函数代码如下:
package hello import ( "gopkg.in/yaml.v3" "io/ioutil" "log" ) // YamlToStruct Yaml文件转struct // file: yaml文件 // s : 要转换的 struct 变量的地址(调用处需要加&) func YamlToStruct(file string, s interface{}) error { // 读取文件 b, err := ioutil.ReadFile(file) if err != nil { log.Print(err) return err } // 转换成Struct err = yaml.Unmarshal(b, s) if err != nil { log.Printf("%v\n", err.Error()) return err } return nil }
调用函数的方式:
先定义一个 struct 的变量,然后把要读取的 yaml 文件的路径和 struct 变量的地址传递给函数。例:
yaml 文件内容为单数(非数组的复杂结构也属于单数)形式时,使用下面的写法。var person Person err := YamlToStruct(file, &person)
yaml 文件内容为复数形式时,则应该是下面的写法。
即变量需要定义成 struct 的切片。var persons []Person err := YamlToStruct(file, &persons)
这里就需要对接口的理解了,接口是什么类型都能传,这里传的是地址。
为什么是传地址呢?
我的理解(纯属个人理解)是,yaml 转成 struct 时,需要知道这个 stuct 是个什么结构。
在方法调用前,定义了一个 struct 的变量,这个变量就会以 stuct 的结构去分配内存空间。
在函数内部,yaml.Unmarshal 处理时,可以根据变量分配的地址,知道如何把 yaml 的内容转换成对应的 struct 结构。
当转换完成后,在外部直接访问 struct 的变量时,变量对应的地址里已经被设置上了对应的内容。
突然顿悟到了接口的强大。
下面以两个 struct 和三个 yaml 文件为例,确认一下调用的结果。
// Family 家
type Family struct {
Name string `yaml:"name"`
Member []Person `yaml:"member"`
}
// Person 人
type Person struct {
Name string `yaml:"name"`
Sex string `yaml:"sex"`
Age int `yaml:"age"`
}
person.yaml (单数形式)
name: panda
sex: M
age: 22
persons.yaml (复数形式)
- name: panda
sex: M
age: 22
- name: tigress
sex: F
age: 20
- name: viper
sex: F
age: 18
- name: monkey
sex: M
age: 22
family.yaml (复杂结构)
name: love my family
member:
- name: sun
sex: M
age: 37
- name: nana
sex: F
age: 32
- name: rain
sex: M
age: 4
调用的测试类:
package hello
import (
"fmt"
"testing"
)
// 单数
func TestYaml_01(t *testing.T) {
file := "person.yaml"
// 在调用方法前定义结构体的变量
var person Person
// 结构体变量的地址传递给 YamlToStruct 方法
err := YamlToStruct(file, &person)
if err != nil {
fmt.Println(err)
}
fmt.Println(person)
fmt.Println(person.Name)
}
// 数组
func TestYaml_02(t *testing.T) {
file := "persons.yaml"
// 在调用方法前定义结构体的变量
// 对应的 Yaml 文件的内容是数组时,变量需要定义成切片。
var persons []Person
// 结构体切片变量的地址传递给 YamlToStruct 方法
err := YamlToStruct(file, &persons)
if err != nil {
fmt.Println(err)
}
fmt.Println(persons)
for _, v := range persons {
fmt.Println(v.Name)
}
}
// 复杂结构
func TestYaml_03(t *testing.T) {
file := "family.yaml"
// 在调用方法前定义结构体的变量
var family Family
// 结构体变量的地址传递给 YamlToStruct 方法
err := YamlToStruct(file, &family)
if err != nil {
fmt.Println(err)
}
fmt.Println(family)
fmt.Println(family.Name)
for _, v := range family.Member {
fmt.Println(v.Name)
}
}
三个测试函数的结果是:
=== RUN TestYaml_01
{panda M 22}
panda
--- PASS: TestYaml_01 (0.00s)
=== RUN TestYaml_02
[{panda M 22} {tigress F 20} {viper F 18} {monkey M 22}]
panda
tigress
viper
monkey
--- PASS: TestYaml_02 (0.00s)
=== RUN TestYaml_03
{love my family [{sun M 37} {nana F 32} {rain M 4}]}
love my family
sun
nana
rain
--- PASS: TestYaml_03 (0.00s)
从结果可以发现,能从文件中读取内容,并转换成正确的 struct。