| Tested on | RHEL 10.2 with Go 1.26.5 |
|---|---|
| Package | go 1.26.5go.yaml.in/yaml/v4 v4.0.0-rc.6 |
| Applies to | Any host with Go 1.23+ installed |
| Privilege | Normal user |
| Scope | Read and parse YAML files in Go: package choice (legacy gopkg.in vs go.yaml.in), Load into structs and maps, nested objects and lists, multi-document loading, WithKnownFields strict parsing, and common errors. Does not cover Helm, Kubernetes manifests, or full YAML specification. |
| Related guides | Parse JSON in Go JSON Unmarshal in Go Golang Viper Type assertions in Go Getting started with Go |
YAML is not part of the Go standard library. To read a YAML file in Go you add a third-party module, load the bytes, and decode them into a typed value. When you know the schema—a config file, CI settings, or service definitions—a struct with yaml tags is usually the clearest model. A map[string]any is better when keys are unknown or the document is truly dynamic.
This walkthrough uses go.yaml.in/yaml/v4 with the current Load / NewLoader API and covers struct parsing, nested lists, dynamic maps, multi-document streams, and strict field checks.
Which YAML package should you use in Go?
| Import path | Status |
|---|---|
gopkg.in/yaml.v3 |
Historical import many modules still use. Frozen legacy branch with security fixes only. |
go.yaml.in/yaml/v3 |
Same v3 API under the maintained module path. Also frozen with security fixes only. |
go.yaml.in/yaml/v4 |
Active development line. Recommended for new projects; still a release candidate (v4.0.0-rc.6 at time of writing). |
v1, v2, and v3 remain available as frozen legacy lines. You do not need to rush migration if stable v3 code already works—plan a move to go.yaml.in/yaml/v4 when you want current features or are starting a new module. See the v3 to v4 migration guide for import-path and API changes. v4 still exposes yaml.Unmarshal and yaml.NewDecoder as compatibility APIs; the examples below use yaml.Load and yaml.NewLoader, which upstream recommends for new v4 code.
Install the YAML package
Create a module if you do not have one yet:
go mod init yamlappgo mod init writes a new go.mod in the current directory.
Add the maintained v4 release used in this article:
go get go.yaml.in/yaml/v4@v4.0.0-rc.6The added line in the output confirms the module resolved. Import it as:
import "go.yaml.in/yaml/v4"| Input shape | Recommended destination |
|---|---|
| Known schema | Struct with yaml tags |
| Dynamic or unknown keys | map[string]any |
Multiple --- documents in one file |
yaml.NewLoader + loop Load until io.EOF |
| Known struct schema where unknown keys must fail | yaml.WithKnownFields() on Load / NewLoader into a struct |
Read a YAML file into a Go struct
Most golang parse yaml file tasks follow one path: read bytes from disk, load into a struct, then use typed fields.
Create config.yaml beside your program:
name: orders-api
port: 8080
enabled: true
database:
host: db.internal
port: 5432Define a struct whose exported fields and tags match the YAML keys, then load:
package main
import (
"fmt"
"os"
"go.yaml.in/yaml/v4"
)
type Database struct {
Host string `yaml:"host"`
Port int `yaml:"port"`
}
type Config struct {
Name string `yaml:"name"`
Port int `yaml:"port"`
Enabled bool `yaml:"enabled"`
Database Database `yaml:"database"`
}
func main() {
b, err := os.ReadFile("config.yaml")
if err != nil {
panic(err)
}
var cfg Config
if err := yaml.Load(b, &cfg); err != nil {
panic(err)
}
fmt.Printf("%+v\n", cfg)
fmt.Println("db host:", cfg.Database.Host)
}Run it from the directory that contains config.yaml:
go run .Sample output:
{Name:orders-api Port:8080 Enabled:true Database:{Host:db.internal Port:5432}}
db host: db.internalA successful load does not prove every required setting is present—missing keys become Go zero values ("", 0, false). Validate business rules after parsing.
YAML tags and exported fields
YAML keys are often lowercase or snake_case. Map them with tags such as Port int `yaml:"port"` or NodeName string `yaml:"node-name"` when the wire name differs from the Go field name.
Only exported (capitalized) fields participate in loading. Lowercase fields are invisible to the decoder, which looks like “YAML ignored half my struct.” The same rule applies to JSON Unmarshal in Go.
Parse nested YAML and lists
Extend the config shape with nested objects and sequences. YAML mappings become structs (or nested struct literals), and YAML sequences become slices:
| YAML shape | Go type |
|---|---|
| mapping / object | struct |
| sequence | slice ([]string, []Server, …) |
| nested mapping | nested struct |
optional subtree where nil matters |
pointer field (*Database) |
Add list fields to the struct from the previous section:
type Server struct {
Host string `yaml:"host"`
Port int `yaml:"port"`
}
type Config struct {
Name string `yaml:"name"`
Servers []string `yaml:"servers"`
Database struct {
Host string `yaml:"host"`
SSL bool `yaml:"ssl"`
} `yaml:"database"`
Backends []Server `yaml:"backends"`
}Load YAML that includes nested database and list keys:
name: edge
servers:
- api-1
- api-2
database:
host: postgres.local
ssl: true
backends:
- host: cache-1
port: 6379
- host: cache-2
port: 6379After yaml.Load, len(cfg.Servers) is 2, cfg.Database.Host is postgres.local, and cfg.Backends[0].Port is 6379. Lists of scalars map to []string or []int; lists of objects map to []YourStruct.
Parse YAML into map[string]any
When the schema is unknown—exploratory tooling, loosely defined documents, or keys you cannot freeze at compile time—load into map[string]any:
const src = `
name: probe
labels:
env: staging
team: platform
`
var m map[string]any
if err := yaml.Load([]byte(src), &m); err != nil {
panic(err)
}
fmt.Println("name:", m["name"])
labels := m["labels"].(map[string]any)
fmt.Println("env:", labels["env"])Nested maps decode as map[string]any; sequences decode as []any. You walk the tree with type assertions (see type assertions in Go).
Maps trade compile-time safety for flexibility. Dynamic scalar types depend on the YAML value and the destination type—they are not the same as encoding/json, where numbers in map[string]any commonly become float64. For compatibility, go-yaml recognizes YAML 1.1 words such as yes/no and on/off when decoding into typed bool fields; otherwise they remain strings. Watch unquoted numeric-looking values: bare 0123 can parse as an octal integer rather than the string you meant. If an ID, ZIP code, or phone number must remain text, model it as string in a struct or quote it in the YAML ("0123").
Decode multiple YAML documents and reject unknown fields
yaml.NewLoader reads from an io.Reader. Use it when a file or stream contains multiple YAML documents separated by ---. For a single document that fits in memory, os.ReadFile plus yaml.Load is enough.
Define a small struct for each document:
type Service struct {
Port int `yaml:"port"`
Host string `yaml:"host"`
}Multiple documents
If the whole multi-document file already fits in memory, v4 can also load all documents into a slice with yaml.Load(data, &docs, yaml.WithAllDocuments()). Use yaml.NewLoader when you want to process documents one at a time from a stream.
Loop until io.EOF—each Load call reads one document:
loader, err := yaml.NewLoader(reader)
if err != nil {
return err
}
for {
var doc Service
err := loader.Load(&doc)
if err == io.EOF {
break
}
if err != nil {
return err
}
process(doc)
}Strict fields with WithKnownFields
By default, loading ignores unknown keys—a typo such as porrt leaves Port at 0 with no error. Pass yaml.WithKnownFields() when mistyped keys must fail:
bad := strings.NewReader("porrt: 8080\nhost: api.local\n")
loader, err := yaml.NewLoader(bad, yaml.WithKnownFields())
if err != nil {
panic(err)
}
var s Service
if err := loader.Load(&s); err != nil {
fmt.Println(err)
}A typo in the key name produces a construct error instead of silently leaving Port at zero:
yaml: construct errors: line 1: field porrt not found in type main.ServiceStill validate required business fields in Go after a successful load.
Common YAML parsing errors
| Problem | Likely reason |
|---|---|
no such file or directory |
Wrong path or working directory when calling os.ReadFile |
| Fields remain empty | Fields are unexported or yaml tags do not match YAML keys |
cannot unmarshal string into int |
Scalar type in the file does not match the Go field type |
| cannot unmarshal mapping into slice | YAML object where the struct expects a list (or the reverse) |
| field not found in type | yaml.WithKnownFields() caught a typo or unsupported key |
| Required value is empty after parsing | Key was missing; Go left the field at its zero value |
yaml: line N: did not find expected ... |
Indentation, colons, or quoting syntax error in the file |
Check both the file read error and the load error. Fix YAML syntax in an editor with YAML linting when line numbers appear in the message.
Summary
This article uses the v4 Load API; yaml.Unmarshal remains available for simple decoding without options. Choose a maintained module (go.yaml.in/yaml/v4 for new work, with gopkg.in/yaml.v3 as the legacy import path), read bytes with os.ReadFile, and decode into a struct or map. Structs with exported fields and yaml tags are the default for known config; nested objects and lists map to nested structs and slices. Use map[string]any only when the document is dynamic, and quote or type scalars carefully when string form matters.
For multiple --- documents, load into a slice with yaml.WithAllDocuments() or stream with yaml.NewLoader and loop until io.EOF. For a known struct schema where typos must fail, pass yaml.WithKnownFields(). Pair parsing with application-level validation so missing keys do not slip through as zero values. For JSON on the same codebase, see Parse JSON in Go.
References
- yaml/go-yaml (maintained Go YAML implementation)
- go.yaml.in/yaml/v4 package documentation
- v3 to v4 migration guide
- os.ReadFile
- io.EOF and Reader behavior

