Go yaml
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
- go-yaml/yaml Official Repository
- gopkg.in/yaml.v3 Documentation
- YAML 1.2 Specification
- YAML Cheat Sheet
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)
}
}