fmt.GoStringer ist das stille Geschwister von fmt.Stringer. Während Stringer über die Methode String() string eine menschenlesbare Darstellung liefert, die von den Verben %v und %s aufgegriffen wird, beantwortet GoStringer über GoString() string die andere Frage: wie sähe dieser Wert aus, wenn ich ihn als Go-Quelltext aufschreiben müsste? Genau diese Repräsentation aktiviert das Verb %#v — gedacht für Debug-Ausgaben, in denen man später per Copy-and-Paste wieder ein gültiges Literal hätte.
Der Unterschied wirkt klein, ist in der Praxis aber wichtig: Stringer formuliert für Menschen, GoStringer formuliert für den Compiler. Eine Farbe wird per %v vielleicht als rot ausgegeben, per %#v aber als color.RGBA{R:0xff, G:0x00, B:0x00, A:0xff} — ein gültiges Go-Literal, das in einer Snapshot-Datei oder einem generierten Codefragment direkt weiterverwendet werden kann.
Das Interface lebt im Paket fmt und besteht aus einer einzigen Methode. Es hat keinerlei Bezug zum io-Paket und nichts mit Stringer zu tun — beide Interfaces sind unabhängig und können auf demselben Typ parallel implementiert werden.
package fmt
// GoStringer wird vom Verb %#v ausgewertet, um eine
// Go-Syntax-Darstellung des Wertes zu erzeugen.
type GoStringer interface {
GoString() string
}Die Rückgabe ist ein gewöhnlicher string. Konventionell enthält sie ein gültiges Go-Literal oder einen Konstruktor-Ausdruck, der den Wert rekonstruieren würde. Es gibt keine technische Prüfung dieser Konvention — der Compiler akzeptiert jeden String. Die Disziplin liegt beim Autor der Methode.
Das Verb %#v ist die „Go-Variante" von %v. Ohne GoStringer-Implementierung erzeugt es eine vom fmt-Paket selbst zusammengestellte Repräsentation in Go-Syntax, inklusive Paketpfad und Feldnamen. Sobald ein Typ GoString() implementiert, übernimmt diese Methode die Kontrolle über die Ausgabe.
package main
import "fmt"
type Point struct {
X, Y int
}
type Tagged struct {
Name string
}
func (t Tagged) GoString() string {
return fmt.Sprintf("main.Tagged(%q)", t.Name)
}
func main() {
p := Point{X: 3, Y: 4}
t := Tagged{Name: "alpha"}
fmt.Printf("%#v\n", p) // Default-Repräsentation
fmt.Printf("%#v\n", t) // GoString() übernimmt
}main.Point{X:3, Y:4}
main.Tagged("alpha")Der Default für Point zeigt das Standardmuster: vollqualifizierter Typname, geschweifte Klammern, Feldname-Wert-Paare. Tagged weicht davon ab und nutzt einen funktionsähnlichen Konstruktor-Stil — beides sind gültige Go-Schreibweisen, die Wahl liegt beim Typ-Autor.
Beide Interfaces lösen verwandte, aber klar getrennte Aufgaben. Die folgende Gegenüberstellung macht die unterschiedlichen Zielgruppen deutlich:
| Aspekt | Stringer | GoStringer |
|---|---|---|
| Methode | String() string | GoString() string |
| Aktiviert durch | %v, %s, Println, Print | %#v |
| Zielgruppe | Menschen, Logs, UI | Entwickler, Debugger, Codegen |
| Konvention | knapp, lesbar | gültiges Go-Literal |
Beispiel Color | rot | color.RGBA{R:0xff, G:0, B:0, A:0xff} |
| Häufigkeit | sehr verbreitet | selten, gezielt eingesetzt |
In den meisten Projekten reicht Stringer. GoStringer lohnt erst dann, wenn die Standardausgabe von %#v entweder zu hässlich, zu lang oder konzeptionell unpassend ist — etwa weil der Typ intern Felder enthält, die nicht zur „Identität" des Wertes gehören.
Die Wahl zwischen Value- und Pointer-Receiver folgt denselben Regeln wie bei jeder anderen Methode. Bei kleinen, unveränderlichen Wertobjekten ist ein Value-Receiver naheliegend; bei größeren Strukturen oder solchen mit eingebetteten Slices/Maps bevorzugt man den Pointer-Receiver, vor allem um unnötige Kopien zu vermeiden.
package main
import "fmt"
type Money struct {
Amount int64 // Minorunits, z.B. Cent
Currency string // ISO 4217
}
// Value-Receiver: Money ist klein und immutable
func (m Money) GoString() string {
return fmt.Sprintf("main.Money{Amount: %d, Currency: %q}",
m.Amount, m.Currency)
}
func main() {
m := Money{Amount: 1990, Currency: "EUR"}
fmt.Printf("%#v\n", m)
}main.Money{Amount: 1990, Currency: "EUR"}Das Muster ist fast immer dasselbe: ein fmt.Sprintf mit dem vollqualifizierten Typnamen als Prefix, gefolgt von den relevanten Feldern mit ihrem jeweils passenden Verb — %q für Strings, %d für Ganzzahlen, %#v rekursiv für eingebettete Strukturen, sofern diese ebenfalls eine sinnvolle Go-Syntax produzieren.
Ohne eigene GoString()-Methode greift das Standardverhalten von %#v. Das Ergebnis ist deterministisch und folgt einer engen Konvention: Paketname, Typname, geschweifte Klammern, kommagetrennte Feldname-Wert-Paare.
package main
import "fmt"
type Foo struct {
X int
Y string
Z []int
}
func main() {
f := Foo{X: 1, Y: "hallo", Z: []int{2, 3, 4}}
fmt.Printf("%#v\n", f)
}main.Foo{X:1, Y:"hallo", Z:[]int{2, 3, 4}}Auffällig ist, dass kein Leerraum zwischen Feldname: und Wert steht — anders, als man es in handgeschriebenem Code typischerweise formatiert. Wer eigene GoString()-Methoden schreibt, darf großzügiger formatieren; das Ergebnis bleibt gültiges Go.
Ein klassischer Fall für GoStringer ist ein Farbtyp. Die Standardausgabe main.RGBA{R:0xff, G:0x00, B:0x00, A:0xff} ist korrekt, aber nicht so kompakt wie ein eigenes Konstruktor-Literal — gerade bei vielen Farben in einer Palette wird das schnell unleserlich.
package main
import "fmt"
type RGBA struct {
R, G, B, A uint8
}
// Standard-%v: menschenlesbares Hex-Tripel
func (c RGBA) String() string {
return fmt.Sprintf("#%02X%02X%02X%02X", c.R, c.G, c.B, c.A)
}
// %#v: rekonstruierbares Go-Literal via Konstruktor
func (c RGBA) GoString() string {
return fmt.Sprintf("main.Hex(0x%02X%02X%02X%02X)", c.R, c.G, c.B, c.A)
}
// Hex ist der zugehoerige Konstruktor — der von GoString
// erzeugte Code wuerde diesen wieder aufrufen.
func Hex(v uint32) RGBA {
return RGBA{
R: uint8(v >> 24),
G: uint8(v >> 16),
B: uint8(v >> 8),
A: uint8(v),
}
}
func main() {
rot := Hex(0xFF0000FF)
fmt.Printf("%v\n", rot) // menschenlesbar
fmt.Printf("%#v\n", rot) // Go-Code
}#FF0000FF
main.Hex(0xFF0000FF)Der Trick liegt in der Symmetrie: GoString() gibt einen Aufruf aus, der den Wert wieder erzeugt. Wer die Ausgabe in einen Test einbettet, hat damit unmittelbar gültigen Go-Quelltext — kein Hin-und-Her-Übersetzen zwischen Hex-Notation und Struct-Initialisierer.
Der zweite typische Einsatz liegt in Werkzeugen, die Go-Code erzeugen oder mit Snapshots arbeiten. Ein Generator für statische Konfiguration, ein Mock-Framework oder ein Snapshot-Test-Runner möchte Werte so ausgeben, dass die Ausgabe direkt als Go-Datei eincheckbar ist.
package main
import (
"fmt"
"strings"
)
type Route struct {
Method string
Path string
Handler string // Name der Handler-Funktion
}
type Router struct {
Routes []Route
}
func (r Route) GoString() string {
return fmt.Sprintf("main.Route{Method: %q, Path: %q, Handler: %q}",
r.Method, r.Path, r.Handler)
}
func (r Router) GoString() string {
var b strings.Builder
b.WriteString("main.Router{\n")
b.WriteString(" Routes: []main.Route{\n")
for _, rt := range r.Routes {
fmt.Fprintf(&b, " %#v,\n", rt)
}
b.WriteString(" },\n")
b.WriteString("}")
return b.String()
}
func main() {
rtr := Router{Routes: []Route{
{"GET", "/users", "ListUsers"},
{"POST", "/users", "CreateUser"},
}}
fmt.Printf("var snapshot = %#v\n", rtr)
}var snapshot = main.Router{
Routes: []main.Route{
main.Route{Method: "GET", Path: "/users", Handler: "ListUsers"},
main.Route{Method: "POST", Path: "/users", Handler: "CreateUser"},
},
}Das Ergebnis ist nicht nur lesbar, sondern auch ein syntaktisch gültiges Go-Literal — bereit, in eine Testdatei kopiert zu werden. Beachtenswert ist die rekursive Verwendung von %#v für die einzelnen Route-Werte; weil Route selbst GoString() implementiert, bleibt die Kontrolle über die Formatierung durchgängig in der eigenen Hand.
Verb-Bindung an %#v
GoStringer wird ausschließlich vom Verb %#v aufgerufen — nicht von %v, %s oder Println. Wer eine alternative Standardausgabe will, muss zusätzlich Stringer implementieren.
Stringer ≠ GoStringer
Stringer zielt auf Menschen, GoStringer auf den Compiler. Beide Interfaces sind unabhängig und können auf demselben Typ koexistieren, mit unterschiedlichem Zweck.
Go-Syntax-Konvention
Die Rückgabe von GoString() sollte ein gültiges Go-Literal oder ein Konstruktor-Ausdruck sein — der ganze Sinn des Verbs liegt in der Rekonstruierbarkeit.
Receiver-Wahl
Für kleine, unveränderliche Wertobjekte passt ein Value-Receiver; bei größeren Strukturen oder eingebetteten Slices/Maps ist ein Pointer-Receiver effizienter.
Default-Output
Ohne GoString() erzeugt %#v automatisch pkg.Typ{Feld:Wert, ...} — bereits brauchbar und oft ausreichend für Debug-Ausgaben.
Rekursionsgefahr
GoString() darf nicht %#v auf demselben Wert aufrufen, sonst entsteht eine unendliche Rekursion. Felder einzeln mit passenden Verben (%q, %d, %#v auf Sub-Werten) formatieren.
Anwendungsfall Debug und Snapshot
Hauptnutzen liegt in Debug-Logging, Snapshot-Tests und Codegeneratoren — überall dort, wo die Ausgabe später als Go-Quelltext weiterverwendet wird.
Selten in idiomatischem Go
In typischen Anwendungen wird GoStringer selten implementiert. Erst bei eigenen Konstruktor-Funktionen, Wertepaletten oder Codegen-Werkzeugen rechnet sich der Aufwand.