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, ©); err != nil {
log.Fatalf("error: %v", err)
}
fmt.Println(schema)
fmt.Println(copy)
}
最後に様々な型をパースしてマッピングして必要な構造体フィールドを抽出してみよう。
package 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, ©); 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
Discussion