Go yaml

シリアライゼーションGoYAML設定ファイル構造化データ

ライブラリ

go-yaml/yaml - Go言語のデファクトスタンダードYAMLライブラリ

概要

go-yaml/yamlは、Go言語で最も幅広く使われているYAMLパーサーです。YAML 1.1とYAML 1.2仕様をサポートし、設定ファイル、データシリアライゼーション、CI/CDパイプラインなど様々な用途で活用されています。構造体タグによる柔軟なマッピング、アンカーとエイリアスの完全サポート、マルチドキュメント処理など、YAML仕様の幅広い機能を実装しています。

詳細

go-yaml/yamlは、gopkg.in/yaml.v3として提供される成熟したYAMLライブラリで、encoding/jsonパッケージと似たAPIを提供します。バージョン3では、ノードベースのAPIが追加され、YAMLドキュメントのプログラマティックな操作が可能になりました。

ライブラリは、YAMLの複雑な機能を網羅的にサポートしています。アンカー(&)とエイリアス(*)による参照機能、複数行文字列の様々な表現方法(|、>、|−、>-など)、カスタムタグによる型安全なデシリアライザーの実装など、高度な機能が含まれています。

特に注目すべき機能として、yaml.Nodeを使用したAST(抽象構文木)操作があります。これにより、YAMLドキュメントの構造を保持したまま変更を加えたり、コメントやフォーマットを保持したラウンドトリップ処理が可能です。

メリット・デメリット

メリット

  • 完全なYAMLサポート: YAML 1.1/1.2仕様の包括的な実装
  • 成熟度: Goコミュニティで最も信頼されているYAMLライブラリ
  • 柔軟なAPI: encoding/jsonと似た直感的なAPI設計
  • ノードAPI: ASTレベルでのYAML操作が可能
  • アンカー/エイリアス: 複雑なYAML構造の完全サポート
  • カスタムマーシャラー: 独自のシリアライゼーションロジック実装可能

デメリット

  • パフォーマンス: JSONよりパースが遅い
  • メモリ使用量: 大きなYAMLファイルでメモリ消費が大きい
  • エラーメッセージ: パースエラーが分かりにくい場合がある
  • 脆弱性リスク: アンカーを使った10億笑い攻撃の可能性
  • 型変換の曖昧さ: YAMLの柔軟な型システムによる予期しない変換
  • 標準ライブラリではない: 外部依存として管理が必要

参考ページ

書き方の例

1. 基本的なYAMLエンコード・デコード

package main

import (
    "fmt"
    "log"
    "gopkg.in/yaml.v3"
)

type Config struct {
    Server struct {
        Host string `yaml:"host"`
        Port int    `yaml:"port"`
    } `yaml:"server"`
    Database struct {
        Driver   string `yaml:"driver"`
        Host     string `yaml:"host"`
        Port     int    `yaml:"port"`
        Username string `yaml:"username"`
        Password string `yaml:"password"`
        DBName   string `yaml:"dbname"`
    } `yaml:"database"`
    Features []string          `yaml:"features"`
    Settings map[string]string `yaml:"settings"`
}

func main() {
    // 構造体からYAMLへ
    config := Config{}
    config.Server.Host = "localhost"
    config.Server.Port = 8080
    config.Database.Driver = "postgres"
    config.Database.Host = "db.example.com"
    config.Database.Port = 5432
    config.Database.Username = "appuser"
    config.Database.Password = "secret"
    config.Database.DBName = "myapp"
    config.Features = []string{"auth", "api", "cache"}
    config.Settings = map[string]string{
        "timeout": "30s",
        "retry":   "3",
    }
    
    yamlData, err := yaml.Marshal(&config)
    if err != nil {
        log.Fatal(err)
    }
    
    fmt.Println("YAML出力:")
    fmt.Println(string(yamlData))
    
    // YAMLから構造体へ
    var decoded Config
    err = yaml.Unmarshal(yamlData, &decoded)
    if err != nil {
        log.Fatal(err)
    }
    
    fmt.Printf("\nデコード結果: %+v\n", decoded)
}

2. アンカーとエイリアスの使用

package main

import (
    "fmt"
    "gopkg.in/yaml.v3"
)

func anchorExample() {
    yamlData := `
defaults: &defaults
  adapter: postgres
  host: localhost
  port: 5432

development:
  <<: *defaults
  database: myapp_development

test:
  <<: *defaults
  database: myapp_test

production:
  <<: *defaults
  host: db.production.com
  database: myapp_production
  pool: 10
`
    
    var result map[string]interface{}
    err := yaml.Unmarshal([]byte(yamlData), &result)
    if err != nil {
        panic(err)
    }
    
    // 結果を再びYAMLに変換して表示
    output, _ := yaml.Marshal(result)
    fmt.Println("アンカー展開後:")
    fmt.Println(string(output))
}

3. カスタムマーシャラーの実装

package main

import (
    "fmt"
    "gopkg.in/yaml.v3"
    "time"
    "strings"
)

type Duration struct {
    time.Duration
}

// YAMLマーシャラーの実装
func (d Duration) MarshalYAML() (interface{}, error) {
    return d.Duration.String(), nil
}

// YAMLアンマーシャラーの実装
func (d *Duration) UnmarshalYAML(value *yaml.Node) error {
    var s string
    if err := value.Decode(&s); err != nil {
        return err
    }
    
    duration, err := time.ParseDuration(s)
    if err != nil {
        return err
    }
    
    d.Duration = duration
    return nil
}

type ServerConfig struct {
    Timeout     Duration `yaml:"timeout"`
    IdleTimeout Duration `yaml:"idle_timeout"`
    MaxLifetime Duration `yaml:"max_lifetime"`
}

func customMarshalerExample() {
    config := ServerConfig{
        Timeout:     Duration{30 * time.Second},
        IdleTimeout: Duration{5 * time.Minute},
        MaxLifetime: Duration{24 * time.Hour},
    }
    
    yamlData, _ := yaml.Marshal(&config)
    fmt.Println("YAML出力:")
    fmt.Println(string(yamlData))
    
    // デコードテスト
    yamlInput := `
timeout: 45s
idle_timeout: 10m
max_lifetime: 48h
`
    var decoded ServerConfig
    yaml.Unmarshal([]byte(yamlInput), &decoded)
    
    fmt.Printf("デコード結果:\n")
    fmt.Printf("Timeout: %v\n", decoded.Timeout.Duration)
    fmt.Printf("IdleTimeout: %v\n", decoded.IdleTimeout.Duration)
    fmt.Printf("MaxLifetime: %v\n", decoded.MaxLifetime.Duration)
}

4. ノードAPIを使ったYAML操作

package main

import (
    "fmt"
    "gopkg.in/yaml.v3"
    "bytes"
)

func nodeAPIExample() {
    yamlData := `
app:
  name: MyApp
  version: 1.0.0
  # これはコメントです
  debug: true

servers:
  - name: web-1
    host: 192.168.1.10
  - name: web-2  
    host: 192.168.1.11
`
    
    var root yaml.Node
    err := yaml.Unmarshal([]byte(yamlData), &root)
    if err != nil {
        panic(err)
    }
    
    // ノードを走査して変更
    modifyNode(&root)
    
    // 変更後のYAMLを出力
    var buf bytes.Buffer
    encoder := yaml.NewEncoder(&buf)
    encoder.SetIndent(2)
    encoder.Encode(&root)
    
    fmt.Println("変更後のYAML:")
    fmt.Println(buf.String())
}

func modifyNode(node *yaml.Node) {
    if node.Kind == yaml.DocumentNode {
        for _, child := range node.Content {
            modifyNode(child)
        }
    } else if node.Kind == yaml.MappingNode {
        for i := 0; i < len(node.Content); i += 2 {
            key := node.Content[i]
            value := node.Content[i+1]
            
            // versionフィールドを更新
            if key.Value == "version" && value.Kind == yaml.ScalarNode {
                value.Value = "2.0.0"
                value.LineComment = "# Updated version"
            }
            
            modifyNode(value)
        }
    } else if node.Kind == yaml.SequenceNode {
        for _, child := range node.Content {
            modifyNode(child)
        }
    }
}

5. ストリーミング処理(複数ドキュメント)

package main

import (
    "fmt"
    "gopkg.in/yaml.v3"
    "io"
    "strings"
)

type Document struct {
    Kind       string            `yaml:"kind"`
    APIVersion string            `yaml:"apiVersion"`
    Metadata   map[string]string `yaml:"metadata"`
    Spec       interface{}       `yaml:"spec"`
}

func streamingExample() {
    // 複数のYAMLドキュメントを含むデータ
    yamlData := `
---
kind: Service
apiVersion: v1
metadata:
  name: web-service
  namespace: default
spec:
  ports:
    - port: 80
      targetPort: 8080
---
kind: Deployment
apiVersion: apps/v1
metadata:
  name: web-deployment
  namespace: default
spec:
  replicas: 3
  selector:
    matchLabels:
      app: web
---
kind: ConfigMap
apiVersion: v1
metadata:
  name: web-config
  namespace: default
data:
  config.yaml: |
    server:
      port: 8080
`
    
    decoder := yaml.NewDecoder(strings.NewReader(yamlData))
    
    var docs []Document
    for {
        var doc Document
        err := decoder.Decode(&doc)
        if err == io.EOF {
            break
        }
        if err != nil {
            panic(err)
        }
        
        docs = append(docs, doc)
        fmt.Printf("ドキュメント: %s/%s - %s\n", 
            doc.APIVersion, doc.Kind, doc.Metadata["name"])
    }
    
    fmt.Printf("\n合計 %d 個のドキュメントを読み込みました\n", len(docs))
}

6. エラーハンドリングとバリデーション

package main

import (
    "fmt"
    "gopkg.in/yaml.v3"
    "errors"
)

type AppConfig struct {
    Name    string `yaml:"name" validate:"required"`
    Version string `yaml:"version" validate:"required,semver"`
    Port    int    `yaml:"port" validate:"required,min=1,max=65535"`
    Debug   bool   `yaml:"debug"`
}

// UnmarshalYAML カスタムバリデーション付き
func (c *AppConfig) UnmarshalYAML(value *yaml.Node) error {
    type plain AppConfig
    if err := value.Decode((*plain)(c)); err != nil {
        return err
    }
    
    // バリデーション
    if c.Name == "" {
        return errors.New("name is required")
    }
    if c.Port < 1 || c.Port > 65535 {
        return errors.New("port must be between 1 and 65535")
    }
    
    return nil
}

func errorHandlingExample() {
    // 正常なケース
    validYAML := `
name: MyApplication
version: 1.2.3
port: 8080
debug: false
`
    
    var config AppConfig
    err := yaml.Unmarshal([]byte(validYAML), &config)
    if err != nil {
        fmt.Printf("エラー: %v\n", err)
    } else {
        fmt.Printf("正常に読み込み: %+v\n", config)
    }
    
    // エラーケース
    invalidYAML := `
name: ""
version: 1.2.3
port: 70000
debug: false
`
    
    var invalidConfig AppConfig
    err = yaml.Unmarshal([]byte(invalidYAML), &invalidConfig)
    if err != nil {
        fmt.Printf("\nバリデーションエラー: %v\n", err)
    }
    
    // YAML構文エラーの例
    syntaxErrorYAML := `
name: MyApp
version: 1.2.3
  port: 8080  # インデントエラー
`
    
    err = yaml.Unmarshal([]byte(syntaxErrorYAML), &config)
    if err != nil {
        fmt.Printf("\n構文エラー: %v\n", err)
    }
}