Skip to content
gohacks.dev
Basics

Print a Struct in Go in a Debug Friendly Way Quickly

Print a struct with field names using %+v, as Go syntax with %#v, or as indented JSON for nested data, and learn why pointer fields print as addresses.

3 min read
Summarize in AI
Flat illustration of an editor window full of code.

You have a struct and you want to see what is in it. The default fmt.Println(u) works and tells you very little, a bare list of values with no hint which is which. Three small changes to the verb give you something you can debug with, and one more tool covers the nested case that the verbs handle badly. This is part of the Go basics path, and the verbs are all in the fmt reference.

Tested with Go 1.25.6, macOS on Apple Silicon (arm64)

The three verbs

main.goGo
package main

import (
	"encoding/json"
	"fmt"
)

type Address struct {
	City string
	Zip  string
}

type User struct {
	Name    string
	Age     int
	Tags    []string
	Meta    map[string]int
	Home    *Address
	Work    Address
	private bool
}

func main() {
	u := User{
		Name: "ada",
		Age:  36,
		Meta: map[string]int{"logins": 3},
		Work: Address{"London", "N1"},
	}

	fmt.Printf("%v\n", u)
	fmt.Printf("%+v\n", u)
	fmt.Printf("%#v\n", u)

	out, _ := json.MarshalIndent(u, "", "  ")
	fmt.Println(string(out))
}
Text
{ada 36 [] map[logins:3] <nil> {London N1} false}
{Name:ada Age:36 Tags:[] Meta:map[logins:3] Home:<nil> Work:{City:London Zip:N1} private:false}
main.User{Name:"ada", Age:36, Tags:[]string(nil), Meta:map[string]int{"logins":3}, Home:(*main.Address)(nil), Work:main.Address{City:"London", Zip:"N1"}, private:false}
{
  "Name": "ada",
  "Age": 36,
  "Tags": null,
  "Meta": {
    "logins": 3
  },
  "Home": null,
  "Work": {
    "City": "London",
    "Zip": "N1"
  }
}

Read the four outputs in order. %v prints {ada 36 [] map[logins:3] <nil> {London N1} false}, which is hard to match with the fields. %+v adds the names, Name:ada Age:36 Tags:[], and this is the one I use when debugging. %#v prints the type and the syntax you would write, including []string(nil) for the nil Tags, which is the verb that tells a nil slice from an empty one, and (*main.Address)(nil) for the nil pointer. Use it when the question is about nil versus empty.

The JSON form indents every level, which beats any of the verbs once the struct has nested values. Notice what it does not show: the unexported field private is gone, because encoding/json cannot see it, and Tags prints as null for a nil slice. So use the verbs when you want everything and JSON when you want readable shape.

Pointers print as addresses

A pointer to a struct at the top level prints well: fmt.Printf("%+v", &u) gives &{Name:ada ...}. A pointer field inside a struct does not. If Home pointed at an address, %+v would print Home:0x14000010150, a hex address that changes on every run and says nothing about the contents. To see through it, print the pointed-to value, fmt.Printf("%+v", *u.Home), or switch to JSON, which follows pointers. The libraries spew and go-cmp follow them as well, and the standard library does not, by design, since following pointers can loop forever on cyclic data.

When String hides the fields

If the type has a String() string method, %v and %s call it, and that is probably not what you want while debugging. %+v calls String too, because the method takes precedence for any verb that accepts a string. To see the fields of such a type, print %#v, which does not call String unless the type has a GoString method. Time values are a common case: fmt.Println(time.Now()) shows a formatted time and not the internal fields.

Take them out before you commit

Debug prints stay in code longer than anyone plans. A better habit is to log with log/slog and pass the struct as an attribute, slog.Info("created", "user", u), which the handler renders consistently and which can be switched off by level. Check your diff for Printf("%+v" before committing. The reference for all the verbs is the fmt package documentation, and encoding/json's MarshalIndent covers the JSON route.

Taggedquick tip
Share

Keep reading

Get new articles by email

We send one email when a new article is published. No spam.

Unsubscribe in one click. No spam, ever.