Golang Read YAML File: Parse YAML into Struct, Map, or Decoder

Tech reviewed: Deepak Prasad
Golang Read YAML File: Parse YAML into Struct, Map, or Decoder

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:

text
go get gopkg.in/yaml.v3

Import path:

go
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:

text
read file bytes → unmarshal YAML → use Go value

For 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.

yaml
# 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"
go
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 → []YourStruct or []map[string]any if each row varies.
go
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.

go
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):

go
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

  1. Inspect the YAML keys and nesting (objects vs lists vs scalars).
  2. Map each object to a struct (or map[string]any only for that subtree if it varies).
  3. Map each sequence to a slice ([]T or []map[string]any).
  4. Pick Go field types that match how you use the value (int, string, bool, time.Duration via custom unmarshaling if needed).
  5. 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 ---.

go
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.

go
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.ReadFile first. For Unmarshal, an empty document often leaves the destination at zero values or a nil map with nil error; do not assume “no error” means “good config.”
  • Missing keys: unmarshaling leaves the corresponding Go field at its zero value ("", 0, nil pointers, nil slices). Validate required settings after parse.
go
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 field string so you never rely on map[string]any numeric decoding.
  • Booleans: unquoted yes / no / on / off may become bools under YAML 1.1–style rules—if you need the word as a string, quote it.
  • Numbers: bare 8080 decodes cleanly into int; into any inside a map you may still see float64 in 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:

go
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—stdlib json.Unmarshal / json.Decoder; common for APIs and package.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.ReadFileyaml.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.v3
  • os.ReadFile
  • Snippet verifier in this repo: examples/golang-parse-yaml-verify (run go test -v after go mod download).

Frequently Asked Questions

1. Which YAML library should I use for golang yaml in new code?

Use gopkg.in/yaml.v3 for current APIs and bug fixes; v2 still appears in older modules but v3 is the better default for new examples and projects.

2. How do I read a YAML file in Go?

Read bytes with os.ReadFile for small or medium files, then yaml.Unmarshal into a struct or map; for streams or multiple documents separated by ---, use yaml.NewDecoder on an io.Reader and call Decode in a loop.

3. How do I reject unknown YAML keys when unmarshaling into a struct?

yaml.Unmarshal ignores unknown fields by default; use yaml.NewDecoder on a reader, call KnownFields(true) on the decoder, then Decode into your struct so typos in the file surface as errors.

4. Why do my YAML map values become float64 for integers?

When unmarshaling into map[string]any, scalars follow YAML typing rules and numbers often decode as float64; use a typed struct, yaml.Node, or quote scalars in YAML so IDs and phone numbers stay strings.

5. How does YAML parsing differ from JSON in Go?

JSON uses the standard library encoding/json; YAML needs a third-party module such as gopkg.in/yaml.v3—YAML is common for config files while JSON is common for APIs; see the parse JSON guide linked from this page for the JSON side.
Tuan Nguyen

Data Scientist

Proficient in Golang, Python, Java, MongoDB, Selenium, Spring Boot, Kubernetes, Scrapy, API development, Docker, Data Scraping, PrimeFaces, Linux, Data Structures, and Data Mining. With expertise spanning these technologies, he develops …

  • Deep Learning with TensorFlow
  • Machine Learning with Python
  • Go (programming language)
  • Python (programming language)
  • Java (programming language)
  • MongoDB