Featured image of post Read the specified YAML file with golang

Read the specified YAML file with golang

GoでYAMLファイルを読み込んで要素を取り出す

日本語版/Japanese

Summary

The following code is an example of reading a YAML file with the name specified by a command line flag.

$ cat members.yaml
members:
- name: "John Doe"
  age: 20
  privileged: false
  languages:
  - language: "Java"
    years: 4
  - language: "Go"
    years: 2
- name: "Jane Doe"
  age: 30
  privileged: true

$ go run main.go -yaml-file members.yaml
name: John Doe
age: 20
privileged: false
language 0: Java for 4 year(s)
language 1: Go for 2 year(s)
name: Jane Doe
age: 30
privileged: true

Prerequisites

  • go 1.18

Detail

  • Parse a flag with the flag package
  • Read a YAML file with the os package
  • Convert YAML values into a Go struct with the yaml package

Parse a flag

Use flag package to parse command line flags.
In this case, the number of flags must be 1 and no arg is allowed.

var yamlFile string
flag.StringVar(&yamlFile, "yaml-file", "", "path to the YAML file")
flag.Parse()
if flag.NFlag() != 1 || flag.NArg() != 0 {
    log.Fatalf("you must give one flag and no arg: #(flags)=%v, #(args)=%v\n", flag.NFlag(), flag.NArg())
}
if yamlFile == "" {
    log.Fatalf("you must specify yaml file\n")
}

Read a file

os.Readfile can open a file to read.

// yamlFile: string
data, err := os.ReadFile(yamlFile)
if err != nil {
    log.Fatalf("ERROR in reading file: %v\n", err)
}

Convert YAML values into a Go struct

yaml package converts YAML values into a Go struct.
In this case, assume the following YAML and define a struct representing values.

members:
- name: "John Doe"
  age: 20
  privileged: false
  languages:
  - language: "Java"
    years: 4
  - language: "Go"
    years: 2
- name: "Jane Doe"
  age: 30
  privileged: true

Some fields can be omitted if there is no need to read it.
map[interface{}]interface{} can be used instead of the struct, but I do not like that way.

// data: []byte
y := struct {
    Members []struct {
        Name       string `yaml:"name"`
        Age        int    `yaml:"age"`
        Privileged bool   `yaml:"privileged"`
        Languages  []struct {
            Language string `yaml:"language"`
            Years    int    `yaml:"years"`
        } `yaml:"languages"`
    } `yaml:"members"`
}{}
err = yaml.Unmarshal(data, &y)
if err != nil {
    log.Fatalf("ERROR in parsing YAML: %v\n", err)
}

Then the values can be accessed by the field name of the struct representing the key name.

fmt.Printf("name: %v\n", y.Members[0].Name)

(The following is the same content in Japanese.)

まとめ

コマンドラインで指定されたYAMLファイルを開いて要素を取り出すまでのGoコードは以下のように書けそう
(以下コピペ用)

$ cat members.yaml
members:
- name: "John Doe"
  age: 20
  privileged: false
  languages:
  - language: "Java"
    years: 4
  - language: "Go"
    years: 2
- name: "Jane Doe"
  age: 30
  privileged: true

$ go run main.go -yaml-file members.yaml
name: John Doe
age: 20
privileged: false
language 0: Java for 4 year(s)
language 1: Go for 2 year(s)
name: Jane Doe
age: 30
privileged: true

環境

  • go 1.18

詳細

  • flagで指定されたYAMLファイル名をコード内で使用する
  • 指定されたファイルを開く
  • ファイルの内容を構造体にパースしてkeyで値を取り出せる状態にする

flagの処理

まずは標準のflagパッケージを使い、実行時に対象のファイル名を指定できるようにする。
4行目以降でflag数と入力値のバリデーションをかけてみたが、特に気にならなければ消してもいい。

var yamlFile string
flag.StringVar(&yamlFile, "yaml-file", "", "path to the YAML file")
flag.Parse()
if flag.NFlag() != 1 || flag.NArg() != 0 {
    log.Fatalf("you must give one flag and no arg: #(flags)=%v, #(args)=%v\n", flag.NFlag(), flag.NArg())
}
if yamlFile == "" {
    log.Fatalf("you must specify yaml file\n")
}

ファイルの読み込み

次にosパッケージで対象のファイルを開く。

// yamlFile: string
data, err := os.ReadFile(yamlFile)
if err != nil {
    log.Fatalf("ERROR in reading file: %v\n", err)
}

YAMLにパース

最後にyamlパッケージで対象のyamlを構造体にパースする。

今回は下記のようなYAMLを想定し、
その構造をパース用の構造体に定義する。

members:
- name: "John Doe"
  age: 20
  privileged: false
  languages:
  - language: "Java"
    years: 4
  - language: "Go"
    years: 2
- name: "Jane Doe"
  age: 30
  privileged: true

パースが不要な要素については定義しなくても問題ない。
また、構造体の代わりにmap[interface{}]interface{}を使う方法もあるが、
要素を呼び出すたびに型をアサーションして使うのはあまり好きではない。

// data: []byte
y := struct {
    Members []struct {
        Name       string `yaml:"name"`
        Age        int    `yaml:"age"`
        Privileged bool   `yaml:"privileged"`
        Languages  []struct {
            Language string `yaml:"language"`
            Years    int    `yaml:"years"`
        } `yaml:"languages"`
    } `yaml:"members"`
}{}
err = yaml.Unmarshal(data, &y)
if err != nil {
    log.Fatalf("ERROR in parsing YAML: %v\n", err)
}

構造体にパースできたら、
あとはフィールド名を指定して要素にアクセスができる。

fmt.Printf("name: %v\n", y.Members[0].Name)

おわり

久しぶりにGoに触ったらいろいろ消耗してしまったので自分のコピペ用に書いてみた。

YAMLを設定ファイルとして読み込むものを書く機会が多いので、出番があるといいな…

おまけ

ソファの裏から出てくるねこ