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.
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})
}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.
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})
}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.
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})
}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.
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)
}%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 |
|---|---|---|---|
int | 42 | 42 | 42 |
string | hallo | hallo | "hallo" |
bool | true | true | true |
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.
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)
}%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
%+vfür Logs. Selbsterklärend dank Feldnamen, kompakt genug für Log-Zeilen, respektiert Stringer-Implementierungen. Default für jedenlog.Printfundslog-Attr.%#vfür Test-Failures und Bug-Reports. Reproduzierbar, kopier- und einfügbar, mit vollqualifizierten Typnamen.%vfü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.
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",
})
}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.
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)
}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 &.
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)
}%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.