Go yaml

SerializationGoYAMLConfiguration FilesStructured Data

Library

go-yaml/yaml - The De Facto Standard YAML Library for Go

Overview

go-yaml/yaml is the most widely used YAML parser for the Go language. Supporting both YAML 1.1 and YAML 1.2 specifications, it is utilized in various applications including configuration files, data serialization, and CI/CD pipelines. It implements extensive YAML features including flexible mapping through struct tags, complete support for anchors and aliases, multi-document processing, and more.

Details

go-yaml/yaml, provided as gopkg.in/yaml.v3, is a mature YAML library that offers an API similar to the encoding/json package. Version 3 introduced a node-based API, enabling programmatic manipulation of YAML documents while preserving their structure.

The library comprehensively supports YAML's complex features. This includes reference functionality through anchors (&) and aliases (*), various representations of multi-line strings (|, >, |−, >-, etc.), and implementation of type-safe deserializers through custom tags. It also includes advanced features for handling YAML's sophisticated capabilities.

A particularly notable feature is AST (Abstract Syntax Tree) manipulation using yaml.Node. This enables modifications to YAML documents while preserving their structure, and allows for round-trip processing that maintains comments and formatting.

Advantages and Disadvantages

Advantages

  • Complete YAML Support: Comprehensive implementation of YAML 1.1/1.2 specifications
  • Maturity: Most trusted YAML library in the Go community
  • Flexible API: Intuitive API design similar to encoding/json
  • Node API: YAML manipulation at the AST level
  • Anchors/Aliases: Complete support for complex YAML structures
  • Custom Marshalers: Ability to implement custom serialization logic

Disadvantages

  • Performance: Slower parsing compared to JSON
  • Memory Usage: High memory consumption for large YAML files
  • Error Messages: Parse errors can sometimes be unclear
  • Vulnerability Risk: Potential for billion laughs attack using anchors
  • Type Conversion Ambiguity: Unexpected conversions due to YAML's flexible type system
  • Not Standard Library: Requires management as external dependency

References

Code Examples

1. Basic YAML Encode and Decode

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() {
    // Struct to 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 Output:")
    fmt.Println(string(yamlData))
    
    // YAML to Struct
    var decoded Config
    err = yaml.Unmarshal(yamlData, &decoded)
    if err != nil {
        log.Fatal(err)
    }
    
    fmt.Printf("\nDecoded: %+v\n", decoded)
}

2. Using Anchors and Aliases

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)
    }
    
    // Convert result back to YAML for display
    output, _ := yaml.Marshal(result)
    fmt.Println("After anchor expansion:")
    fmt.Println(string(output))
}

3. Implementing Custom Marshalers

package main

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

type Duration struct {
    time.Duration
}

// Implement YAML marshaler
func (d Duration) MarshalYAML() (interface{}, error) {
    return d.Duration.String(), nil
}

// Implement YAML unmarshaler
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 Output:")
    fmt.Println(string(yamlData))
    
    // Decode test
    yamlInput := `
timeout: 45s
idle_timeout: 10m
max_lifetime: 48h
`
    var decoded ServerConfig
    yaml.Unmarshal([]byte(yamlInput), &decoded)
    
    fmt.Printf("Decoded:\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. YAML Manipulation with Node API

package main

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

func nodeAPIExample() {
    yamlData := `
app:
  name: MyApp
  version: 1.0.0
  # This is a comment
  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)
    }
    
    // Traverse and modify nodes
    modifyNode(&root)
    
    // Output modified YAML
    var buf bytes.Buffer
    encoder := yaml.NewEncoder(&buf)
    encoder.SetIndent(2)
    encoder.Encode(&root)
    
    fmt.Println("Modified 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]
            
            // Update version field
            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. Streaming Processing (Multiple Documents)

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() {
    // Data containing multiple YAML documents
    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("Document: %s/%s - %s\n", 
            doc.APIVersion, doc.Kind, doc.Metadata["name"])
    }
    
    fmt.Printf("\nTotal %d documents loaded\n", len(docs))
}

6. Error Handling and Validation

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 with custom validation
func (c *AppConfig) UnmarshalYAML(value *yaml.Node) error {
    type plain AppConfig
    if err := value.Decode((*plain)(c)); err != nil {
        return err
    }
    
    // Validation
    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() {
    // Valid case
    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("Error: %v\n", err)
    } else {
        fmt.Printf("Successfully loaded: %+v\n", config)
    }
    
    // Error case
    invalidYAML := `
name: ""
version: 1.2.3
port: 70000
debug: false
`
    
    var invalidConfig AppConfig
    err = yaml.Unmarshal([]byte(invalidYAML), &invalidConfig)
    if err != nil {
        fmt.Printf("\nValidation error: %v\n", err)
    }
    
    // YAML syntax error example
    syntaxErrorYAML := `
name: MyApp
version: 1.2.3
  port: 8080  # Indentation error
`
    
    err = yaml.Unmarshal([]byte(syntaxErrorYAML), &config)
    if err != nil {
        fmt.Printf("\nSyntax error: %v\n", err)
    }
}