Golang で YAML をシリアライズ、デシリアライズする


Golang で YAML をシリアライズ、デシリアライズする

こんにちは、最近 YAML をなんとなく触っていたのですが、いまいち理解していない部分もあり、また IaC の勉強を始めてみたり、 Golang でデシリアライズする必要が出てきたりと、今後のことを考えてここで解説メモを残しておこうと思いました。

YAMLについて

YAML Ain't Markup Language (YAML) はシリアライズ言語、
YAML はこの数年で着実に進化しており、わりと人気が出てきているイメージです。

YAML のメリットはコメントを記述できたり、インデントで区切っている点など、何より人間にとって読みやすいというのが大きなメリットだと思います。

YAML は主に何に使われるかというと、設定ファイルのフォーマットに使われることが多いです。
このシリアライズする言語の能力に関して JSON のような言語に置き換えられることがよく見られてます。

今回はYAMLの文法を Golang でデシリアライズしながら見ていきたいと思います。

サンプルファイルで YAML を見ていきましょう

まずデシリアライズの前に適当に YAML でサンプルを書いてみたいと思います。

 hoge: "hogehoge"
 gaga: 'gagagaga'
 huga: hugahuga
 life: 788400.5
 birthday: true
 count: 3
 calling-item:
   - hoge
   - gaga
   - huga
 day-01:
   calling-item: three
   action:
     sleep: 7
     move: 3
     eat: 0.5

YAML におけるデータ型

YAML には主に4つのデータ型があります。

  • strings
  • integer
  • floating-point number
  • boolean

まず先頭の hoge , gaga , huga は適当に命名していますが、いずれも strings です。
YAML では引用符による区別はないので、この点に関しては、基本的に自由に記述できますが、引用符のない数字は例外です。
つまり、引用符で囲まれていない数値を整数または浮動小数点として認識します。

calling-item:
   - hoge
   - gaga
   - huga

この記述方法は配列です。
calling-item には4つの要素があり、それぞれの要素をハイフンで表します。
それから、2つのスペースによるインデントは、 YAML がネストを示す方法。

スペースの数はファイルによって異なりますが、タブは使用できないです。

 day-01:
   calling-item: three
   action:
     sleep: 7
     move: 3
     eat: 0.5

最後に、day-01 があります。 day-01 にはさらに要素があり、それぞれがインデントされています。

day-01 は、1つの文字列、および別の要素を含む要素として見ることができます。

YAML はキー値のネストと型の混合定義をサポートしています。

次にイメージしやすいように json に置き換えてみてみましょう。
このようにネストされた部分も{}で括ってくれるので、イメージはしやすいですが、時に読みづらいと思うので、 YAML の文法を知っていれば YAML の方が可読性が上がりますね。

{
  "hoge": "hogehoge",
  "gaga": 'gagaga',
  "huga": hugahuga,
  "life": 788400.5,
  "birthday": true,
  "count": 3,
  "calling-item": [
     "hoge",
     "gaga",
     "huga",
  ],
  "day-01": {
  "calling-item": "three",
    "action": {
      "sleep": 3,
      "move": 5,
      "eat": 0.5
    }
  }
}

それぞれの型をデシリアライズしてみる

Golang でデシリアライズする方法はいくつか存在しますが、今回は go-yaml パッケージを使ってデシリアイズをやってみたいと思います。

このツールを使う上で少しわかりにくい予約語がある→( Marshal , Unmarshal )

  • Marshal 整列させる、整理する
  • Unmarshal 無秩序な状態にする

なので、 parse や mapping 等わかりやすい関数名にして一連の Marshal , Unmarshal の処理をまとめるとぐっとわかりやすく、直感的にフィールドを操作できるだろう。

一連の処理を共通関数にしてみる。

Marshal , Unmarshal を理解して使いこなすには色々と手続きを踏まなければならないので、面倒である。
なので様々な型のデータをGolangの構造体の各種型のフィールドに Marshal , Unmarsha してくれるようにまとめておくと使い勝手が良い。

また、パースしたのちに必要なフィールドのみ抽出したい場合があると思うが、単純に抽出したい場合があると思うが、その際にも一連の Marshal , Unmarshal 処理も関数化しておくと非常に便利である。

package main

import (
    "errors"
    "fmt"
    "log"
    "reflect"

    "gopkg.in/yaml.v2"
)

var data = `
a: test
b: 6
`

func parse(data string, dest interface{}) error {
    if reflect.TypeOf(dest).Kind() != reflect.Ptr {
        return errors.New("mapping: dest is not pointer")
    }
    parseError := yaml.Unmarshal([]byte(data), dest)
    return parseError
}

func mapping(source interface{}, copy interface{}) error {
    data, sourceError := yaml.Marshal(source)
    if sourceError != nil {
        return sourceError
    }
    if reflect.TypeOf(copy).Kind() != reflect.Ptr {
        return errors.New("mapping: copy is not pointer")
    }
    destError := yaml.Unmarshal(data, copy)
    return destError
}

func main() {
    var schema struct {
        A string
        B int
    }
    var copy struct {
        A string
    }
    if err := parse(data, &schema); err != nil {
        log.Fatalf("error: %v", err)
    }
    if err := mapping(&schema, &copy); err != nil {
        log.Fatalf("error: %v", err)
    }
    fmt.Println(schema)
    fmt.Println(copy)
}

最後に様々な型をパースしてマッピングして必要な構造体フィールドを抽出してみよう。

ackage main

import (
    "errors"
    "fmt"
    "log"
    "reflect"

    "gopkg.in/yaml.v2"
)

var data = `
a: test
b: 0
c: true
d: 3.14
e: 00
f:
   - hoge
   - huga
g:
   - 0
   - 1
   - 2
`

func parse(source string, dest interface{}) error {
    if reflect.TypeOf(dest).Kind() != reflect.Ptr {
        return errors.New("mapping: dest is not pointer")
    }
    parseError := yaml.Unmarshal([]byte(source), dest)
    return parseError
}

func mapping(source interface{}, dest interface{}) error {
    data, sourceError := yaml.Marshal(source)
    if sourceError != nil {
        return sourceError
    }
    if reflect.TypeOf(dest).Kind() != reflect.Ptr {
        return errors.New("mapping: dest is not pointer")
    }
    destError := yaml.Unmarshal(data, dest)
    return destError
}

func main() {
    var schema struct {
        A string
        B int
        C bool
        D float32
        E byte
        F []string
        G []int
    }
    var copy struct {
        A string
        F []string
    }
    if err := parse(data, &schema); err != nil {
        log.Fatalf("error: %v", err)
    }
    if err := mapping(schema, &copy); err != nil {
        log.Fatalf("error: %v", err)
    }
    fmt.Println(schema)
    fmt.Println(copy)
}

結果は次のようになる。

{test 0 true 3.14 0 [hoge huga] [0 1 2]}
{test [hoge huga]}

本来の YAML から Unmarshal した構造体、

var schema struct {
        A string
        B int
        C bool
        D float32
        E byte
        F []string
        G []int
    }

から、必要なフィールドを持った構造体「 copy 」に一旦 YAML にシリアライズしてから「 copy 」へデシリアライズすることができる。

(↑ YAML への一時的なシリアライズの様子)

まとめ

go-yaml を使えば、 YAML を特に難しいことを意識することなくある程度自由に操作できますね。

また同様に JSON に関してもこのような感じでencoding/jsonを用いて Marshal , Unmarshal できます。

Web開発ではよくフロントエンドからバックエンドへ JSON 形式でデータを渡すことが多いと思うので JSON の Marshal , Unmarshal 操作も同様に覚えておくと良さそうです。

ここまでお読みいただきありがとうございました。

参考

「yaml-tutorial」 https://www.cloudbees.com/blog/yaml-tutorial-everything-you-need-get-started

「go-yaml/yaml」 https://github.com/go-yaml/yaml

「golang/go」 https://github.com/golang/go/blob/master/src/encoding/json/encode.go

「json」 https://pkg.go.dev/encoding/json