Drei Verben, ein Wert, drei sehr unterschiedliche Ausgaben. %v, %+v und %#v sind die drei Schweizer Taschenmesser für das Inspizieren von Werten in Go — und obwohl sie sich auf den ersten Blick fast gleich anfühlen, leisten sie ganz unterschiedliche Arbeit.

Die Faustregel vorweg: %v ist die kompakte Default-Form für den schnellen Blick, %+v ergänzt bei Structs die Feldnamen und ist damit der Sweet-Spot für Logs, %#v liefert eine Go-Syntax-Repräsentation, die als gültiger Quellcode wieder eingesetzt werden kann — ideal für Test-Failures und Bug-Reports.

%v — die kompakte Default-Form

%v ist das Allzweck-Verb. Bei primitiven Typen verhält es sich exakt wie das jeweils passende spezialisierte Verb: ein int wird wie mit %d ausgegeben, ein string wie mit %s, ein float64 wie mit %g, ein bool wie mit %t. Spannend wird es bei zusammengesetzten Typen. Ein Struct wird mit %v in geschweiften Klammern dargestellt, allerdings ohne Feldnamen — nur die Werte stehen in der Reihenfolge der Felddeklaration.

Go default.go
package main

import "fmt"

type User struct {
	Name string
	Age  int
}

func main() {
	fmt.Printf("%v\n", 42)
	fmt.Printf("%v\n", "hallo")
	fmt.Printf("%v\n", 3.14)
	fmt.Printf("%v\n", true)
	fmt.Printf("%v\n", User{"Anna", 30})
	fmt.Printf("%v\n", []int{1, 2, 3})
	fmt.Printf("%v\n", map[string]int{"a": 1})
}
Output
42
hallo
3.14
true
{Anna 30}
[1 2 3]
map[a:1]

Bei diesem Mini-Struct ist {Anna 30} noch lesbar, bei einem realen API-Modell mit fünfzehn Feldern wird die Ausgabe wertlos.

%+v — der Debug-Sweet-Spot

%+v ist das stille Arbeitstier der Go-Logs. Bei primitiven Typen verhält es sich identisch zu %v. Der entscheidende Effekt kommt erst bei Structs: dort werden die Feldnamen mit ausgegeben, in der Form {Name:Anna Age:30}. Damit ist die Ausgabe selbsterklärend, ohne dass man parallel die Struct-Definition aufschlagen muss.

Go plus.go
package main

import "fmt"

type User struct {
	Name string
	Age  int
}

func main() {
	fmt.Printf("%+v\n", 42)
	fmt.Printf("%+v\n", "hallo")
	fmt.Printf("%+v\n", User{"Anna", 30})
	fmt.Printf("%+v\n", []int{1, 2, 3})
	fmt.Printf("%+v\n", map[string]int{"a": 1})
}
Output
42
hallo
{Name:Anna Age:30}
[1 2 3]
map[a:1]

Bei Slices und Maps bringt %+v gegenüber %v keinen Vorteil — der Plus-Effekt greift nur auf der Struct-Ebene. Verschachtelte Structs in Slices profitieren rekursiv: jedes innere Struct erhält seine Feldnamen.

%#v — die Go-Syntax-Repräsentation

%#v produziert einen String, der als gültiger Go-Quellcode wieder kompiliert werden könnte. Structs bekommen ihren vollqualifizierten Typnamen mit Paketpräfix (main.User{Name:"Anna", Age:30}), Strings werden in Anführungszeichen gesetzt, Slices erhalten ihre []T{...}-Form, Maps ihre map[K]V{...}-Form.

Go hash.go
package main

import "fmt"

type User struct {
	Name string
	Age  int
}

func main() {
	fmt.Printf("%#v\n", 42)
	fmt.Printf("%#v\n", "hallo")
	fmt.Printf("%#v\n", true)
	fmt.Printf("%#v\n", User{"Anna", 30})
	fmt.Printf("%#v\n", []int{1, 2, 3})
	fmt.Printf("%#v\n", map[string]int{"a": 1, "b": 2})
}
Output
42
"hallo"
true
main.User{Name:"Anna", Age:30}
[]int{1, 2, 3}
map[string]int{"a":1, "b":2}

Beachtenswert: %v und %+v geben den String ohne Quotes aus, %#v mit — denn nur mit Quotes wäre der Ausdruck wieder gültiger Go-Code.

Direkter Vergleich an einem komplexen Wert

Theorie ist gut, der Side-by-Side-Blick auf denselben verschachtelten Wert ist besser.

Go vergleich.go
package main

import "fmt"

type Address struct {
	City string
	Zip  string
}

type Profile struct {
	Name    string
	Address *Address
	Tags    []string
	Meta    map[string]int
}

func main() {
	p := Profile{
		Name:    "Anna",
		Address: &Address{City: "Berlin", Zip: "10115"},
		Tags:    []string{"admin", "beta"},
		Meta:    map[string]int{"score": 42},
	}

	fmt.Printf("%%v:  %v\n", p)
	fmt.Printf("%%+v: %+v\n", p)
	fmt.Printf("%%#v: %#v\n", p)
}
Output
%v:  {Anna 0xc0000140c0 [admin beta] map[score:42]}
%+v: {Name:Anna Address:0xc0000140c0 Tags:[admin beta] Meta:map[score:42]}
%#v: main.Profile{Name:"Anna", Address:(*main.Address)(0xc0000140c0), Tags:[]string{"admin", "beta"}, Meta:map[string]int{"score":42}}

%v ist die Telegrammfassung — kurz, aber ohne den Pointer kann niemand sagen, was hinter 0xc0000140c0 steckt. %+v macht das Lesen angenehm. %#v ist die Maximalform: vollqualifizierter Typname, Pointer als Konversion, String-Literale mit Quotes, Slice- und Map-Typen explizit ausgeschrieben.

Auch hier ein Detail: alle drei Verben dereferenzieren den Pointer nicht. Sie zeigen die Adresse, nicht den Inhalt von *Address.

Verhalten bei verschiedenen Typen

Typ%v%+v%#v
int424242
stringhallohallo"hallo"
booltruetruetrue
struct{A int; B string}{1 x}{A:1 B:x}main.T{A:1, B:"x"}
[]int{1,2,3}[1 2 3][1 2 3][]int{1, 2, 3}
map[string]int{"a":1}map[a:1]map[a:1]map[string]int{"a":1}
*T (Pointer)0xc000...0xc000...(*main.T)(0xc000...)
nil<nil><nil><nil>

Zwei Muster zeigen sich klar. Erstens: bei allem, was kein Struct ist, liefern %v und %+v denselben Output. Zweitens: %#v ist immer ausführlicher.

Stringer-Interface — wer gewinnt?

Sobald ein Typ String() string implementiert, gewinnt die String()-Methode bei %v und %+v. %#v hingegen ignoriert Stringer und liefert weiterhin die Go-Syntax-Form.

Go stringer.go
package main

import "fmt"

type User struct {
	Name string
	Age  int
}

func (u User) String() string {
	return fmt.Sprintf("User<%s, %d>", u.Name, u.Age)
}

func (u User) GoString() string {
	return fmt.Sprintf("NewUser(%q, %d)", u.Name, u.Age)
}

func main() {
	u := User{Name: "Anna", Age: 30}

	fmt.Printf("%%v:  %v\n", u)
	fmt.Printf("%%+v: %+v\n", u)
	fmt.Printf("%%#v: %#v\n", u)
}
Output
%v:  User<Anna, 30>
%+v: User<Anna, 30>
%#v: NewUser("Anna", 30)

Wer auch %#v beeinflussen will, implementiert zusätzlich fmt.GoStringer mit GoString() string.

Auswahl-Faustregel

  • %+v für Logs. Selbsterklärend dank Feldnamen, kompakt genug für Log-Zeilen, respektiert Stringer-Implementierungen. Default für jeden log.Printf und slog-Attr.
  • %#v für Test-Failures und Bug-Reports. Reproduzierbar, kopier- und einfügbar, mit vollqualifizierten Typnamen.
  • %v für triviale Typen und Stringer-Werte. Kurz, knackig, ohne Overhead.

Wer unsicher ist, greift im Zweifel zu %+v.

HTTP-Handler-Log mit %+v

In einem HTTP-Handler will man im Debug-Modus alle Felder eines eingegangenen Request-DTOs sehen. Mit %v wäre die Log-Zeile ein Werte-Salat. Mit %+v wird sie zur sprechenden Diagnose.

Go handler.go
package main

import (
	"fmt"
	"log"
)

type CreateUserRequest struct {
	Email      string
	Name       string
	Age        int
	Newsletter bool
	Referrer   string
}

func handle(req CreateUserRequest) {
	log.Printf("incoming request: %v", req)
	log.Printf("incoming request: %+v", req)
}

func main() {
	log.SetFlags(0)
	handle(CreateUserRequest{
		Email:      "anna@example.com",
		Name:       "Anna",
		Age:        30,
		Newsletter: true,
		Referrer:   "twitter",
	})
}
Output
incoming request: {anna@example.com Anna 30 true twitter}
incoming request: {Email:anna@example.com Name:Anna Age:30 Newsletter:true Referrer:twitter}

Die erste Zeile zwingt jeden Lesenden, parallel die Struct-Definition zu öffnen. Die zweite Zeile ist self-contained.

Test-Failure-Reproduktion mit %#v

Test-Frameworks profitieren massiv von %#v. Wenn ein Vergleich fehlschlägt, will der Tester den exakten Wert sehen — am liebsten so, dass er ihn als Konstante in einen neuen Regressionstest kopieren kann.

Go assert.go
package main

import (
	"fmt"
	"reflect"
)

type Order struct {
	ID    int
	Items []string
	Total float64
}

func assertEqual(got, want any) {
	if !reflect.DeepEqual(got, want) {
		fmt.Printf("MISMATCH\n  got:  %#v\n  want: %#v\n", got, want)
		return
	}
	fmt.Println("OK")
}

func main() {
	got := Order{ID: 1, Items: []string{"book"}, Total: 19.99}
	want := Order{ID: 1, Items: []string{"book", "pen"}, Total: 21.50}

	assertEqual(got, want)
}
Output
MISMATCH
  got:  main.Order{ID:1, Items:[]string{"book"}, Total:19.99}
  want: main.Order{ID:1, Items:[]string{"book", "pen"}, Total:21.5}

Der entscheidende Vorteil: Beide Zeilen sind wieder kompilierbarer Go-Code. Wer das Problem reproduzieren will, kopiert main.Order{...} direkt in einen neuen Test als Eingabewert.

Pointer-Behandlung — Vorsicht

Ein häufiger Stolperstein: Keines der drei Verben dereferenziert Pointer automatisch, wenn der Pointer als Struct-Feld auftaucht. Wer einen *User als Top-Level-Argument übergibt, sieht hingegen den Wert mit vorangestelltem &.

Go pointer.go
package main

import "fmt"

type User struct {
	Name string
	Age  int
}

func main() {
	p := &User{Name: "Anna", Age: 30}

	fmt.Printf("%%v   am Pointer:    %v\n", p)
	fmt.Printf("%%+v  am Pointer:    %+v\n", p)
	fmt.Printf("%%#v  am Pointer:    %#v\n", p)

	fmt.Printf("%%v   am Wert:       %v\n", *p)
	fmt.Printf("%%+v  am Wert:       %+v\n", *p)
	fmt.Printf("%%#v  am Wert:       %#v\n", *p)
}
Output
%v   am Pointer:    &{Anna 30}
%+v  am Pointer:    &{Name:Anna Age:30}
%#v  am Pointer:    &main.User{Name:"Anna", Age:30}
%v   am Wert:       {Anna 30}
%+v  am Wert:       {Name:Anna Age:30}
%#v  am Wert:       main.User{Name:"Anna", Age:30}

Der Unterschied: Steht der Pointer als Top-Level-Argument, wird er sinnvoll formatiert; steckt er als Feld in einem anderen Struct, sieht man nur die Adresse.

Interessantes

%v

Kompakt, Default-Form für primitive Typen, respektiert Stringer.String(). Bei Structs ohne Feldnamen — praktisch für triviale Typen, mühsam für komplexe Modelle.

%+v

Identisch zu %v bei allem außer Structs — dort werden die Feldnamen mit ausgegeben ({Name:Anna Age:30}). Sweet-Spot für Logs und schnelle Diagnose.

%#v

Liefert eine Go-Syntax-Repräsentation, die als gültiger Quellcode reproduzierbar ist. Erste Wahl für Test-Failures und Bug-Reports.

%#v ignoriert Stringer

%#v greift String() nicht ab, sondern nutzt — falls vorhanden — GoStringer.GoString().

Pointer werden in Feldern nicht dereferenziert

Alle drei Verben zeigen Pointer-Adressen, sobald der Pointer in einem Feld steckt. Wer den Inner-Wert sehen will, schreibt *p im Aufruf.

Maps sortiert seit Go 1.12

Seit Go 1.12 gibt fmt Maps mit nach Keys sortierter Reihenfolge aus — Output ist dadurch deterministisch und gut diff-bar in Tests und Logs.

%+v rekursiv

Bei verschachtelten Structs wirkt %+v durch alle Ebenen — jedes innere Struct erhält ebenfalls seine Feldnamen.

%#v zeigt Paketpräfix

Typnamen werden voll qualifiziert ausgegeben (main.User, pkg.Order). Das macht Outputs aus mehreren Paketen eindeutig zuordenbar.

Weiterführende Ressourcen

Externe Quellen

/ Weiter

Zurück zu Das fmt-Paket — Formatierte I/O

Zur Übersicht