Yaml #
YAML (YAML Ain’t Markup Language) adalah format serialisasi data yang lebih mudah dibaca manusia dibanding JSON — tidak ada tanda kurung kurawal, tidak ada koma, indentasi menentukan struktur. YAML sangat populer untuk file konfigurasi: Kubernetes manifests, GitHub Actions, Docker Compose, Ansible playbooks — semuanya menggunakan YAML. Go tidak menyertakan parser YAML di standard library, tapi gopkg.in/yaml.v3 adalah library yang matang dan menjadi standar de-facto di ekosistem Go.
Instalasi #
go get gopkg.in/yaml.v3
Sintaks YAML — Ringkasan Cepat #
Sebelum masuk ke Go, pahami sintaks YAML yang akan sering ditemui:
# Komentar dengan #
# String — tidak perlu quote kecuali ada karakter spesial
nama: Budi Santoso
kota: "Jakarta: Pusat" # quote karena ada titik dua
# Angka
umur: 28
harga: 15000000
pi: 3.14159
# Boolean (case-insensitive)
aktif: true
terverifikasi: false
# Null
email: null
catatan: ~ # alternatif null
# Array (block style — satu elemen per baris)
skills:
- Go
- Python
- Docker
# Array (flow style — satu baris)
tags: [backend, api, microservice]
# Object nested
alamat:
jalan: Jl. Merdeka No. 1
kota: Jakarta
kode_pos: "10110" # string, bukan integer
# Array of objects
produk:
- id: 1
nama: Laptop
harga: 15000000
- id: 2
nama: Mouse
harga: 350000
# Multi-line string
deskripsi: |
Baris pertama.
Baris kedua.
Baris ketiga dengan newline di akhir setiap baris.
ringkasan: >
Semua baris ini akan digabung
menjadi satu baris panjang
dengan spasi sebagai pemisah.
# Anchor (&) dan alias (*) — reuse nilai
defaults: &defaults
timeout: 30
retry: 3
production:
<<: *defaults # merge defaults
host: prod.example.com
Marshal — Go ke YAML #
import "gopkg.in/yaml.v3"
type Config struct {
Server ServerConfig `yaml:"server"`
Database DatabaseConfig `yaml:"database"`
Features []string `yaml:"features"`
}
type ServerConfig struct {
Host string `yaml:"host"`
Port int `yaml:"port"`
Debug bool `yaml:"debug"`
}
type DatabaseConfig struct {
URL string `yaml:"url"`
MaxConns int `yaml:"max_connections"`
Timeout int `yaml:"timeout_seconds"`
}
cfg := Config{
Server: ServerConfig{
Host: "0.0.0.0",
Port: 8080,
Debug: false,
},
Database: DatabaseConfig{
URL: "postgres://localhost/myapp",
MaxConns: 25,
Timeout: 30,
},
Features: []string{"auth", "payments", "notifications"},
}
data, err := yaml.Marshal(cfg)
if err != nil {
log.Fatal(err)
}
fmt.Println(string(data))
Output:
server:
host: 0.0.0.0
port: 8080
debug: false
database:
url: postgres://localhost/myapp
max_connections: 25
timeout_seconds: 30
features:
- auth
- payments
- notifications
Unmarshal — YAML ke Go #
yamlContent := `
server:
host: localhost
port: 9090
debug: true
database:
url: postgres://localhost/testdb
max_connections: 10
timeout_seconds: 5
features:
- auth
- payments
`
var cfg Config
if err := yaml.Unmarshal([]byte(yamlContent), &cfg); err != nil {
log.Fatal("gagal parse YAML:", err)
}
fmt.Printf("Server: %s:%d (debug=%v)\n",
cfg.Server.Host, cfg.Server.Port, cfg.Server.Debug)
fmt.Printf("Database: %s\n", cfg.Database.URL)
fmt.Printf("Features: %v\n", cfg.Features)
Membaca dari File #
func loadConfig(path string) (*Config, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("baca file config: %w", err)
}
var cfg Config
if err := yaml.Unmarshal(data, &cfg); err != nil {
return nil, fmt.Errorf("parse YAML: %w", err)
}
return &cfg, nil
}
Struct Tags YAML #
Tag YAML bekerja mirip dengan JSON tapi dengan sintaks yaml:"...":
type AppConfig struct {
// Rename field
AppName string `yaml:"app_name"`
// omitempty: skip jika zero value
DebugMode bool `yaml:"debug,omitempty"`
LogLevel string `yaml:"log_level,omitempty"`
// "-": selalu skip
InternalSecret string `yaml:"-"`
// inline: "flatten" field dari struct lain ke level ini
CommonConfig `yaml:",inline"`
// flow: gunakan flow style (satu baris) untuk marshal
Tags []string `yaml:"tags,flow"`
}
type CommonConfig struct {
Timeout int `yaml:"timeout"`
Retry int `yaml:"retry"`
}
// Contoh dengan inline:
// timeout: 30 ← dari CommonConfig (di-inline, bukan nested)
// retry: 3
// tags: [a, b, c] ← flow style
Custom Marshaler dan Unmarshaler #
Implementasikan interface yaml.Marshaler dan yaml.Unmarshaler untuk kontrol penuh:
// Duration yang serializable ke format "30s", "5m", "1h"
type Duration struct {
time.Duration
}
func (d Duration) MarshalYAML() (interface{}, error) {
return d.String(), nil // "1m30s", "5s", dll
}
func (d *Duration) UnmarshalYAML(value *yaml.Node) error {
var s string
if err := value.Decode(&s); err != nil {
return err
}
dur, err := time.ParseDuration(s)
if err != nil {
return fmt.Errorf("format durasi tidak valid %q: %w", s, err)
}
d.Duration = dur
return nil
}
// Penggunaan
type ServerConfig struct {
Host string `yaml:"host"`
ReadTimeout Duration `yaml:"read_timeout"`
WriteTimeout Duration `yaml:"write_timeout"`
ShutdownTimeout Duration `yaml:"shutdown_timeout"`
}
// YAML input
yamlStr := `
host: localhost
read_timeout: 5s
write_timeout: 10s
shutdown_timeout: 30s
`
var srv ServerConfig
yaml.Unmarshal([]byte(yamlStr), &srv)
fmt.Println(srv.ReadTimeout.Duration) // 5s sebagai time.Duration
Multi-Document YAML #
YAML mendukung beberapa dokumen dalam satu file, dipisahkan dengan ---:
// Kubernetes-style multi-document
multiDoc := `
---
kind: Service
name: api-server
port: 8080
---
kind: Database
name: postgres
port: 5432
---
kind: Cache
name: redis
port: 6379
`
decoder := yaml.NewDecoder(strings.NewReader(multiDoc))
for {
var doc map[string]interface{}
if err := decoder.Decode(&doc); err != nil {
if err == io.EOF {
break
}
log.Fatal(err)
}
fmt.Printf("Kind: %s, Name: %s\n", doc["kind"], doc["name"])
}
YAML vs JSON — Kapan Pakai Mana #
GUNAKAN YAML untuk:
✓ File konfigurasi yang diedit manusia (config.yaml, docker-compose.yml)
✓ Infrastructure as Code (Kubernetes, Ansible, Terraform)
✓ CI/CD pipeline definition (GitHub Actions, GitLab CI)
✓ Dokumentasi API (OpenAPI/Swagger)
✓ Template dengan komentar dan anchor/alias
GUNAKAN JSON untuk:
✓ API request/response (JSON lebih universal)
✓ Penyimpanan data (database, cache)
✓ Komunikasi antar service (JSON lebih cepat di-parse)
✓ Package.json, go.mod-style config (tidak butuh komentar)
✓ Ketika struktur sangat dalam — YAML indent bisa membingungkan
Perbandingan fitur:
YAML JSON
Komentar (#) ✓ Tidak ada
Anchor/alias ✓ Tidak ada
Multi-line str ✓ Terbatas
Type coercion ✓ Tidak ada
Keterbacaan ✓ Lebih verbose
Parsing speed Lebih lambat Lebih cepat
Keamanan Lebih riskan Lebih aman
YAML bisa berbahaya jika parsing file dari sumber tidak dipercaya. YAML spec lama mendukung tipe eksekusi (!!python/object) dan fitur berbahaya lain.gopkg.in/yaml.v3sudah aman untuk file konfigurasi biasa, tapi jangan gunakanyaml.Unmarshalkeinterface{}untuk input dari pengguna tanpa validasi.
Contoh Program Lengkap — Config Loader #
Program berikut membangun config loader yang type-safe dengan validasi, nilai default, dan dukungan environment variable:
package main
import (
"fmt"
"log"
"os"
"strings"
"time"
"gopkg.in/yaml.v3"
)
// ── Config Types ──────────────────────────────────────────────
type Duration struct{ time.Duration }
func (d Duration) MarshalYAML() (interface{}, error) { return d.String(), nil }
func (d *Duration) UnmarshalYAML(v *yaml.Node) error {
var s string
if err := v.Decode(&s); err != nil {
return err
}
dur, err := time.ParseDuration(s)
if err != nil {
return fmt.Errorf("durasi tidak valid %q: %w", s, err)
}
d.Duration = dur
return nil
}
type AppConfig struct {
App AppSection `yaml:"app"`
HTTP HTTPSection `yaml:"http"`
Database DatabaseSection `yaml:"database"`
Redis RedisSection `yaml:"redis"`
Log LogSection `yaml:"log"`
}
type AppSection struct {
Name string `yaml:"name"`
Version string `yaml:"version"`
Env string `yaml:"env"` // development, staging, production
}
type HTTPSection struct {
Host string `yaml:"host"`
Port int `yaml:"port"`
ReadTimeout Duration `yaml:"read_timeout"`
WriteTimeout Duration `yaml:"write_timeout"`
ShutdownTimeout Duration `yaml:"shutdown_timeout"`
CORS CORSConfig `yaml:"cors"`
}
type CORSConfig struct {
Enabled bool `yaml:"enabled"`
Origins []string `yaml:"origins"`
}
type DatabaseSection struct {
Host string `yaml:"host"`
Port int `yaml:"port"`
Name string `yaml:"name"`
User string `yaml:"user"`
Password string `yaml:"password"`
MaxOpenConns int `yaml:"max_open_conns"`
MaxIdleConns int `yaml:"max_idle_conns"`
ConnLifetime Duration `yaml:"conn_lifetime"`
SSLMode string `yaml:"ssl_mode"`
}
func (d DatabaseSection) DSN() string {
return fmt.Sprintf("host=%s port=%d dbname=%s user=%s password=%s sslmode=%s",
d.Host, d.Port, d.Name, d.User, d.Password, d.SSLMode)
}
type RedisSection struct {
Host string `yaml:"host"`
Port int `yaml:"port"`
Password string `yaml:"password"`
DB int `yaml:"db"`
Timeout Duration `yaml:"timeout"`
}
func (r RedisSection) Addr() string {
return fmt.Sprintf("%s:%d", r.Host, r.Port)
}
type LogSection struct {
Level string `yaml:"level"` // debug, info, warn, error
Format string `yaml:"format"` // json, text
Output string `yaml:"output"` // stdout, stderr, file path
}
// ── Default Config ────────────────────────────────────────────
var defaultConfig = AppConfig{
App: AppSection{
Name: "myapp",
Version: "0.0.1",
Env: "development",
},
HTTP: HTTPSection{
Host: "0.0.0.0",
Port: 8080,
ReadTimeout: Duration{5 * time.Second},
WriteTimeout: Duration{10 * time.Second},
ShutdownTimeout: Duration{30 * time.Second},
CORS: CORSConfig{
Enabled: true,
Origins: []string{"*"},
},
},
Database: DatabaseSection{
Host: "localhost",
Port: 5432,
MaxOpenConns: 25,
MaxIdleConns: 5,
ConnLifetime: Duration{30 * time.Minute},
SSLMode: "disable",
},
Redis: RedisSection{
Host: "localhost",
Port: 6379,
DB: 0,
Timeout: Duration{3 * time.Second},
},
Log: LogSection{
Level: "info",
Format: "json",
Output: "stdout",
},
}
// ── Loader ────────────────────────────────────────────────────
func LoadConfig(path string) (*AppConfig, error) {
cfg := defaultConfig // mulai dari default
// Load dari file jika ada
if path != "" {
data, err := os.ReadFile(path)
if err != nil {
if !os.IsNotExist(err) {
return nil, fmt.Errorf("baca config file: %w", err)
}
log.Printf("Config file %q tidak ditemukan, menggunakan default", path)
} else {
if err := yaml.Unmarshal(data, &cfg); err != nil {
return nil, fmt.Errorf("parse config YAML: %w", err)
}
}
}
// Override dari environment variables
overrideFromEnv(&cfg)
// Validasi
if err := validateConfig(&cfg); err != nil {
return nil, fmt.Errorf("config tidak valid: %w", err)
}
return &cfg, nil
}
func overrideFromEnv(cfg *AppConfig) {
envMap := map[string]*string{
"APP_ENV": &cfg.App.Env,
"DB_HOST": &cfg.Database.Host,
"DB_NAME": &cfg.Database.Name,
"DB_USER": &cfg.Database.User,
"DB_PASSWORD": &cfg.Database.Password,
"REDIS_HOST": &cfg.Redis.Host,
"REDIS_PASSWORD": &cfg.Redis.Password,
}
for envKey, target := range envMap {
if val := os.Getenv(envKey); val != "" {
*target = val
}
}
}
func validateConfig(cfg *AppConfig) error {
var errs []string
if cfg.App.Name == "" {
errs = append(errs, "app.name wajib diisi")
}
if cfg.HTTP.Port <= 0 || cfg.HTTP.Port > 65535 {
errs = append(errs, fmt.Sprintf("http.port %d tidak valid", cfg.HTTP.Port))
}
if cfg.Database.Host == "" {
errs = append(errs, "database.host wajib diisi")
}
validLevels := map[string]bool{"debug": true, "info": true, "warn": true, "error": true}
if !validLevels[cfg.Log.Level] {
errs = append(errs, fmt.Sprintf("log.level %q tidak valid", cfg.Log.Level))
}
if len(errs) > 0 {
return fmt.Errorf(strings.Join(errs, "; "))
}
return nil
}
// ── Main ──────────────────────────────────────────────────────
func main() {
// Contoh config YAML
exampleYAML := `
app:
name: toko-online
version: "1.2.0"
env: production
http:
host: "0.0.0.0"
port: 443
read_timeout: 5s
write_timeout: 15s
shutdown_timeout: 30s
cors:
enabled: true
origins:
- https://tokoonline.id
- https://admin.tokoonline.id
database:
host: db.internal
port: 5432
name: toko_production
user: app_user
password: rahasia123
max_open_conns: 50
max_idle_conns: 10
conn_lifetime: 1h
ssl_mode: require
redis:
host: redis.internal
port: 6379
password: redis_secret
db: 0
timeout: 2s
log:
level: info
format: json
output: stdout
`
// Tulis ke file temp untuk demo
tmpFile, _ := os.CreateTemp("", "config-*.yaml")
tmpFile.WriteString(exampleYAML)
tmpFile.Close()
defer os.Remove(tmpFile.Name())
cfg, err := LoadConfig(tmpFile.Name())
if err != nil {
log.Fatal("Gagal load config:", err)
}
fmt.Println("=== Konfigurasi Berhasil Dimuat ===")
fmt.Printf("App : %s v%s (%s)\n", cfg.App.Name, cfg.App.Version, cfg.App.Env)
fmt.Printf("HTTP : %s:%d\n", cfg.HTTP.Host, cfg.HTTP.Port)
fmt.Printf("Read Timeout: %v\n", cfg.HTTP.ReadTimeout.Duration)
fmt.Printf("CORS Origins: %v\n", cfg.HTTP.CORS.Origins)
fmt.Printf("Database DSN: %s\n", cfg.Database.DSN())
fmt.Printf("Redis Addr : %s\n", cfg.Redis.Addr())
fmt.Printf("Log Level : %s (%s)\n", cfg.Log.Level, cfg.Log.Format)
// Marshal kembali ke YAML (untuk verifikasi atau export)
fmt.Println("\n=== Marshal Kembali ke YAML ===")
output, _ := yaml.Marshal(cfg)
fmt.Println(string(output))
}
Ringkasan #
gopkg.in/yaml.v3adalah library YAML standar de-facto di Go —go get gopkg.in/yaml.v3.- Struct tags
yaml:"name"untuk rename,omitemptyuntuk skip zero value,inlineuntuk flatten struct,flowuntuk output satu baris.yaml.Marshaluntuk konversi Go ke YAML;yaml.Unmarshaluntuk YAML ke Go — keduanya bekerja secara reflektif.yaml.NewDecoderuntuk multi-document YAML (dipisahkan---) dan streaming file besar.- Custom
MarshalYAML/UnmarshalYAMLuntuk tipe khusus — perlu implementasikanMarshalYAML() (interface{}, error)danUnmarshalYAML(v *yaml.Node) error.- Selalu mulai dari default config dan override dengan file lalu environment variable — ini pola yang robust untuk aplikasi production.
- Validasi config setelah load — jangan biarkan konfigurasi tidak valid sampai ke runtime.
- Environment variable untuk secret (password, API key) — jangan tulis secret di file YAML yang di-commit ke git.
- YAML untuk konfigurasi, JSON untuk API — ini pembagian tugas yang paling umum dan masuk akal.
- Anchor (
&) dan alias (*) di YAML memungkinkan reuse nilai — berguna untuk multi-environment config tanpa duplikasi.