golang 读取 yaml 文件转 struct 公用函数

3,071 阅读5分钟

「这是我参与11月更文挑战的第1天,活动详情查看:2021最后一次更文挑战」。

前言:

个人在开发小工具时读取 yaml 文件转 stuct 时定义的一个公用函数。
明明很简单的一个函数,因为自己对 golang 的接口理解不够,以至于走了很多弯路。
所以在此简单记录下。

golang 读取 yaml 文件转 struct 公用函数

  1. 网上能查到很多相关的方法,但在搜索过程中,发现都是和其它代码写在一起的,没有定义成单独的函数。
  2. 但是网上查到的大多数方法,不同结构的 yaml 转换成 不同的struct时,定义成不同的函数,结果定义了一堆相似的函数。于是一通抱怨 golang 没有泛型。
  3. 最后发现不同结构的 yaml 转换成 不同的struct时,可以使用接口定义成一个公用函数。明明是自己菜,对接口的理解不足。

  1. 网上能搜索到的方法大致如下。
    使用的 yaml 包是 gopkg.in/yaml.v3

    package 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.yaml

    name: panda
    sex: M
    age: 22
    

  1. 个人做的小项目中,经常有多个地方需要读取 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 没有泛型。
    后来也不知道怎么开窍了,原来把参数改成接口就可以了。


  1. 定义成公用函数。

    函数有两个参数,一个是要读取的目标 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。