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.

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

Go verb_binding.go
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
}
Output
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:

AspektStringerGoStringer
MethodeString() stringGoString() string
Aktiviert durch%v, %s, Println, Print%#v
ZielgruppeMenschen, Logs, UIEntwickler, Debugger, Codegen
Konventionknapp, lesbargültiges Go-Literal
Beispiel Colorrotcolor.RGBA{R:0xff, G:0, B:0, A:0xff}
Häufigkeitsehr verbreitetselten, 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.

Go muster.go
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)
}
Output
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.

Go default.go
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)
}
Output
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.

Go rgba.go
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
}
Output
#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.

Go snapshot.go
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)
}
Output
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.

Weiterführende Ressourcen

Externe Quellen

/ Weiter

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

Zur Übersicht