This guide walks through reading a YAML file in Go and everything that usually comes next: picking a YAML package, loading bytes from disk, unmarshaling into a struct or map, handling nested YAML and lists, streaming with a Decoder, optional strict field checks, and fixing typical errors. It stays practical—no Helm or Kubernetes manifest tour—and uses gopkg.in/yaml.v3 throughout. For JSON instead of YAML, use Parse JSON in Go; for struct tags in general, see structs in Go.
Tested on: Go go1.24.4 linux/amd64; kernel 6.14.0-37-generic.
What is the best YAML parser for Go?
Go’s standard library does not include YAML. For golang yaml parser / go yaml parser work, teams almost always add a module. The default choice in current docs and examples is gopkg.in/yaml.v3: stable Unmarshal / Marshal, a streaming yaml.Decoder, and helpers such as KnownFields on the decoder for config validation.
gopkg.in/yaml.v3 vs gopkg.in/yaml.v2
v3 fixes edge cases, improves compatibility with YAML 1.2 style behavior, and is where new features land (for example strict known fields via the decoder). v2 still appears in older repositories; when you touch legacy code you may keep v2 until you migrate imports and re-test. For new modules and articles, standardize on v3.
Other YAML packages
Alternatives such as github.com/goccy/go-yaml exist for ASTs, path APIs, or performance experiments. This page does not compare them in depth—if you only need read config → Go values, yaml.v3 is the usual path.
Install yaml.v3 in your Go module
From your module root:
go get gopkg.in/yaml.v3Import path:
import "gopkg.in/yaml.v3"Which approach should you use?
| Situation | Prefer |
|---|---|
| Known config schema | Struct with yaml tags |
| Unknown or dynamic keys | map[string]any |
| Nested, predictable layout | Nested structs (or nested maps if you must) |
| Quick inspection or one-off probing | Map + type assertions (type assertions in Go) |
| Catch typos in config keys | yaml.NewDecoder + KnownFields(true) + Decode |
Large file, stream, or multi-document YAML (---) |
yaml.NewDecoder on an io.Reader, loop Decode |
| Single file that fits in memory | os.ReadFile + yaml.Unmarshal |
Read YAML file in Go
The basic flow is:
read file bytes → unmarshal YAML → use Go valueFor many config files, os.ReadFile is enough: it returns []byte and an error. Pass the bytes to yaml.Unmarshal. Always handle both the read error and the parse error.
The struct example below uses the same shape as a small config.yaml you might keep next to your program; the YAML is also shown inline so you can paste into a scratch file or embed as a const while learning.
Parse YAML file into a struct
Define a exported struct whose fields and yaml:"..." tags match the YAML keys. Nested objects become nested structs; lists become slices.
# config.yaml (example on disk next to your main package)
name: Amit Kumar
age: 35
address:
street: 123 Example Street
city: Chennai
state: TN
zip: "763098"
phoneNumbers:
- type: home
number: "0123456789"
- type: work
number: "0987654321"package main
import (
"fmt"
"os"
"gopkg.in/yaml.v3"
)
type Address struct {
Street string `yaml:"street"`
City string `yaml:"city"`
State string `yaml:"state"`
Zip string `yaml:"zip"`
}
type Phone struct {
Type string `yaml:"type"`
Number string `yaml:"number"`
}
type Person struct {
Name string `yaml:"name"`
Age int `yaml:"age"`
Address Address `yaml:"address"`
PhoneNumbers []Phone `yaml:"phoneNumbers"`
}
func main() {
b, err := os.ReadFile("config.yaml")
if err != nil {
panic(err)
}
var p Person
if err := yaml.Unmarshal(b, &p); err != nil {
panic(err)
}
fmt.Printf("%+v\n", p)
fmt.Println("city:", p.Address.City)
for _, ph := range p.PhoneNumbers {
if ph.Type == "home" {
fmt.Println("home:", ph.Number)
}
}
}After a successful run you should see the populated struct, the nested city, and the home phone number. Run locally after go get gopkg.in/yaml.v3 and placing config.yaml beside the program (or change the path).
YAML tags in Go structs
YAML often uses lowercase or snake_case keys. Use tags such as City string `yaml:"city"` or NodeName string `yaml:"node-name"` so the key in the file does not have to match the Go field name.
Exported fields are required
Only capitalized fields participate in unmarshaling. Lowercase fields are invisible to the package, which looks like “YAML ignored half my struct.” Same idea as JSON—see JSON Unmarshal in Go for the parallel rules.
Parse nested YAML into nested structs
Nested YAML objects map to nested structs (or pointers to structs if you need nil subtrees). Lists under a key map to slices of structs or scalars. Keep tags aligned at each level.
The Person / Address / PhoneNumbers example above is the pattern most service configs use: one root struct, nested structs for sections, []Phone for a list of objects.
Parse YAML arrays and lists
YAML sequences map to Go slices:
- List of scalars →
[]string,[]int, etc. - List of mappings →
[]YourStructor[]map[string]anyif each row varies.
package main
import (
"fmt"
"gopkg.in/yaml.v3"
)
type Rule struct {
Port int `yaml:"port"`
Host string `yaml:"host"`
}
type Config struct {
Servers []string `yaml:"servers"`
Rules []Rule `yaml:"rules"`
}
func main() {
const src = `
servers:
- app1
- app2
rules:
- port: 80
host: web
- port: 443
host: webtls
`
var c Config
if err := yaml.Unmarshal([]byte(src), &c); err != nil {
panic(err)
}
fmt.Println(len(c.Servers), c.Rules[0].Port, c.Rules[1].Host)
}You should see two servers and the first rule’s port 80. This pattern covers servers, users, ports, environments, or any repeated block in config.
Parse YAML into a map without a struct
When the schema is unknown or very loose, unmarshal into map[string]any. Nested maps decode as nested map[string]any; sequences decode as []any. You then use type assertions (and sometimes loops—see for loops in Go) to dig in.
package main
import (
"fmt"
"gopkg.in/yaml.v3"
)
func main() {
const src = `
name: Amit Kumar
age: 34
address:
city: Chennai
`
var m map[string]any
if err := yaml.Unmarshal([]byte(src), &m); err != nil {
panic(err)
}
fmt.Println("name:", m["name"])
addr := m["address"].(map[string]any)
fmt.Println("city:", addr["city"])
}Prefer structs when you control the schema: maps trade safety for flexibility and make scalar types harder to keep predictable.
Access nested values from a YAML map
Nested access is manual: assert each level to map[string]any, and for lists to []any then assert elements. That becomes verbose fast and panics if the file changes shape.
Use this when you are debugging or handling truly dynamic documents; for known nested YAML, structs are usually cleaner and catch renames at compile time.
This example walks two levels of map plus a list of maps (each item is another map[string]any):
package main
import (
"fmt"
"gopkg.in/yaml.v3"
)
func main() {
const src = `
region:
countries:
- name: India
code: IN
- name: Japan
code: JP
`
var root map[string]any
if err := yaml.Unmarshal([]byte(src), &root); err != nil {
panic(err)
}
region := root["region"].(map[string]any)
countries := region["countries"].([]any)
first := countries[0].(map[string]any)
second := countries[1].(map[string]any)
fmt.Println(first["name"], first["code"])
fmt.Println(second["name"], second["code"])
}After a local run you should see India IN and Japan JP. A safer pattern for production is the two-value assertion (addr, ok := m["region"].(map[string]any)) so a bad file returns ok == false instead of panicking.
YAML to Go struct: how to design the struct
- Inspect the YAML keys and nesting (objects vs lists vs scalars).
- Map each object to a struct (or
map[string]anyonly for that subtree if it varies). - Map each sequence to a slice (
[]Tor[]map[string]any). - Pick Go field types that match how you use the value (
int,string,bool,time.Durationvia custom unmarshaling if needed). - Add
yaml:"wire_key"tags wherever the YAML name differs from the exported field name.
This is a design workflow, not a separate tool—getting the struct right is most of the work when people search for yaml to golang struct or yaml to go struct and need a repeatable mapping recipe.
Decode large or multi-document YAML files
yaml.NewDecoder reads from an io.Reader. Call Decode once per document when the stream contains multiple YAML documents separated by ---.
package main
import (
"fmt"
"strings"
"gopkg.in/yaml.v3"
)
func main() {
in := strings.NewReader(`app: alpha
port: 8080
---
app: beta
port: 9090
`)
dec := yaml.NewDecoder(in)
var first, second map[string]any
if err := dec.Decode(&first); err != nil {
panic(err)
}
if err := dec.Decode(&second); err != nil {
panic(err)
}
fmt.Println(first["app"], second["app"])
}You should see alpha and beta. For a single large document you still use one Decode into your struct or map; the decoder avoids holding extra APIs beyond what you need from Unmarshal.
Strict YAML parsing with known fields
By default, yaml.Unmarshal (and a plain Decode) ignore unknown keys in the file—typos silently disappear. For config validation, use a yaml.Decoder, enable KnownFields(true), then Decode into your struct so unexpected keys produce an error.
package main
import (
"fmt"
"strings"
"gopkg.in/yaml.v3"
)
type Schema struct {
Port int `yaml:"port"`
Host string `yaml:"host"`
}
func main() {
bad := strings.NewReader("port: 80\ntypoHost: example.com\n")
dec := yaml.NewDecoder(bad)
dec.KnownFields(true)
var s Schema
err := dec.Decode(&s)
fmt.Println(err)
}You should see a non-nil error mentioning an unknown field (exact text depends on yaml.v3 version). KnownFields applies to the decoder path, not to every internal Node.Decode edge case—treat strict mode as “best effort strict,” and still validate required business fields in Go after a successful decode.
Empty YAML input and missing values
- Empty or whitespace-only input: treat like any other I/O result—check
os.ReadFilefirst. ForUnmarshal, an empty document often leaves the destination at zero values or a nil map withnilerror; do not assume “no error” means “good config.” - Missing keys: unmarshaling leaves the corresponding Go field at its zero value (
"",0,nilpointers, nil slices). Validate required settings after parse.
package main
import (
"fmt"
"gopkg.in/yaml.v3"
)
type Config struct {
Name string `yaml:"name"`
Port int `yaml:"port"`
}
func main() {
var m map[string]any
err := yaml.Unmarshal([]byte(""), &m)
if err != nil {
panic(err)
}
fmt.Println("empty doc err:", err, "map is nil:", m == nil)
var c Config
if err := yaml.Unmarshal([]byte("name: api\n"), &c); err != nil {
panic(err)
}
fmt.Printf("missing port: Name=%q Port=%d\n", c.Name, c.Port)
}After a local run, the map case often prints map is nil? true (or an empty non-nil map depending on content); the struct case should show Port still 0 because port was omitted.
Common YAML scalar surprises
- IDs, phone numbers, zip codes: if they must stay strings, quote them in YAML (
"0123") or use a typed struct fieldstringso you never rely onmap[string]anynumeric decoding. - Booleans: unquoted
yes/no/on/offmay become bools under YAML 1.1–style rules—if you need the word as a string, quote it. - Numbers: bare
8080decodes cleanly intoint; intoanyinside a map you may still seefloat64in some paths—prefer structs for predictable scalar types.
The same logical keys can decode with different Go dynamic types depending on quoting and YAML boolean rules:
package main
import (
"fmt"
"gopkg.in/yaml.v3"
)
func main() {
const src = `
ids:
bare: 9876543210
quoted: "9876543210"
flags:
on_word: on
on_quoted: "on"
`
var m map[string]any
if err := yaml.Unmarshal([]byte(src), &m); err != nil {
panic(err)
}
ids := m["ids"].(map[string]any)
fmt.Printf("bare id: %T %v\n", ids["bare"], ids["bare"])
fmt.Printf("quoted id: %T %v\n", ids["quoted"], ids["quoted"])
flags := m["flags"].(map[string]any)
fmt.Printf("on unquoted: %T %v\n", flags["on_word"], flags["on_word"])
fmt.Printf("on quoted: %T %v\n", flags["on_quoted"], flags["on_quoted"])
}After a local run, expect the bare numeric-looking scalar to be a numeric type in the map, the quoted id to remain a string, the unquoted on to decode as bool true, and the quoted "on" to stay a string.
YAML unmarshal errors and fixes
| Symptom | Likely cause | What to try |
|---|---|---|
no such file or directory |
Wrong path or cwd | Pass an absolute path or embed config for tests |
yaml: line N: did not find expected ... |
Indentation or colon issues | Fix indentation; use spaces consistently; validate in an editor |
cannot unmarshal string into int (or similar) |
Scalar type mismatch | Change field type, fix YAML quoting, or use string + strconv |
cannot unmarshal !!map into slice |
Object vs list mismatch | Match YAML shape to struct or []map[string]any |
| unknown field / not found in type | KnownFields(true) |
Fix typo in YAML or add field to struct |
| Fields stay empty | Unexported fields or wrong tags | Export fields; align yaml tags with keys |
YAML struct vs map: which should you use?
| Situation | Prefer |
|---|---|
| Known config schema | Struct |
| Unknown or dynamic keys | map[string]any |
| Nested predictable YAML | Nested structs |
| Quick inspection | Map |
| Validation (keys + types) | Struct + KnownFields where appropriate |
| Stream or multi-document | yaml.NewDecoder |
YAML vs JSON parsing in Go
- JSON:
encoding/json—stdlibjson.Unmarshal/json.Decoder; common for APIs andpackage.json-style data. - YAML: third-party
gopkg.in/yaml.v3; common for config files, CI YAML, and human-edited lists.
For JSON workflows, follow Parse JSON in Go and JSON Unmarshal in Go.
Best practices for parsing YAML in Go
- Prefer yaml.v3 for new examples and modules.
- Prefer structs when the schema is stable; use maps only when the document is truly dynamic.
- Check both file read and unmarshal/decode errors.
- After a successful parse, validate required fields in application code.
- Use
KnownFields(true)on a decoder when mistyped keys must fail fast. - Quote ambiguous scalars in YAML when they must remain strings.
Go YAML parsing cheat sheet
| Task | Approach |
|---|---|
| Add parser to module | go get gopkg.in/yaml.v3 |
| Read a small YAML file | os.ReadFile → yaml.Unmarshal |
| Parse into struct | yaml.Unmarshal(b, &cfg) with yaml tags |
| Parse dynamic YAML | yaml.Unmarshal into map[string]any |
| Nested objects | Nested structs or nested maps |
| Lists | Slice fields ([]T, []string, …) |
Multiple --- documents |
yaml.NewDecoder(r) + repeated Decode |
| Strict / known fields only | dec.KnownFields(true) before Decode |
| Emit YAML | yaml.Marshal |
Summary
Reading a YAML file in Go usually means os.ReadFile (or a decoder on a reader), then yaml.Unmarshal or Decode from gopkg.in/yaml.v3. Structs with yaml tags and exported fields give the clearest model for nested config and lists; map[string]any works for exploratory or dynamic YAML at the cost of assertions and scalar surprises. yaml.NewDecoder covers streams and multi-document files; KnownFields(true) on the decoder helps catch unknown keys during Decode. Pair these patterns with quoted scalars where types matter, validate required keys after a successful parse, and use the error table when something fails. For JSON on the same codebase, use the linked parse JSON guides.
References
gopkg.in/yaml.v3os.ReadFile- Snippet verifier in this repo:
examples/golang-parse-yaml-verify(rungo test -vaftergo mod download).

