Wenn ein Wert in Go menschenlesbar werden soll, fragt das fmt-Paket den Wert höflich, ob er sich nicht selbst beschreiben möchte. Diese Frage ist das fmt.Stringer-Interface — ein Ein-Methoden-Vertrag mit der Signatur String() string. Jeder Typ, der diese Methode trägt, übernimmt damit die Kontrolle über seine eigene Darstellung in %s, %v, %q und der gesamten Print/Sprint-Familie.

Stringer ist deshalb das wichtigste Interface des fmt-Pakets: Es ist die idiomatische Stelle, an der Logs lesbar werden, Enums Namen bekommen, Fehlermeldungen Kontext erhalten und Test-Ausgaben Sinn ergeben. Gleichzeitig versteckt das Interface eine berühmte Falle — ein einziger fmt.Sprintf("%v", t) im eigenen String() reicht, um einen Stack-Overflow zu produzieren. Referenz: pkg.go.dev/fmt#Stringer.

Ein Methode, ein Vertrag

Das Interface ist im fmt-Paket exakt eine Zeile lang. Es definiert genau eine Methode, und Go folgt seinem üblichen Prinzip des strukturellen Typings: Es gibt kein implements-Schlüsselwort, keine Registrierung, keine Annotation. Sobald ein Typ eine Methode String() string besitzt, ist er automatisch fmt.Stringer — egal ob du das Interface jemals importierst. Diese Implizitheit ist Absicht. Sie erlaubt, Interfaces nachträglich für fremde Typen zu erfüllen (solange du sie selbst definierst) und entkoppelt Implementierung von Vertrag radikaler als jedes nominale Typsystem.

Go stringer_definition.go
package main

import "fmt"

// Definition im fmt-Paket:
//   type Stringer interface { String() string }

type Ampel int

const (
	Rot Ampel = iota
	Gelb
	Gruen
)

func (a Ampel) String() string {
	switch a {
	case Rot:
		return "rot"
	case Gelb:
		return "gelb"
	case Gruen:
		return "grün"
	}
	return "unbekannt"
}

func main() {
	var s fmt.Stringer = Gruen
	fmt.Println(s)
	fmt.Println(Rot)
}
Output
grün
rot

Bemerkenswert ist die zweite Zeile in main: Gruen wird einer fmt.Stringer-Variablen zugewiesen — ohne Cast, ohne Deklaration. Der Compiler prüft die Methodenmenge zur Compile-Zeit und akzeptiert die Zuweisung, weil sie passt. Genau diese Eigenschaft macht Stringer in der Praxis so unauffällig nützlich.

Wann fmt String() tatsächlich aufruft

fmt ruft String() nicht immer auf. Die Methode greift nur bei Verben, die eine Wert-Beschreibung erwarten — also %s (String-Repräsentation), %v (Default-Wert), %q (gequotete Variante) und in der Print/Sprint/Fprint-Familie. Bei typ-spezifischen Verben wie %d, %x, %T oder %p wird String() ignoriert — diese Verben adressieren die rohen Bytes oder Metadaten des Wertes.

Go wann_stringer_greift.go
package main

import "fmt"

type ID int

func (i ID) String() string {
	return fmt.Sprintf("ID#%04d", int(i)) // int(i) — nicht i selbst!
}

func main() {
	id := ID(42)

	fmt.Printf("%s\n", id)
	fmt.Printf("%v\n", id)
	fmt.Printf("%q\n", id)
	fmt.Println(id)

	fmt.Printf("%d\n", id)
	fmt.Printf("%x\n", id)
	fmt.Printf("%T\n", id)
}
Output
ID#0042
ID#0042
"ID#0042"
ID#0042
42
2a
main.ID

Beachte die Konvertierung int(i) im String()-Body: Ohne sie würde %04d versuchen, eine ID zu formatieren, und fmt würde erneut String() aufrufen — die berüchtigte Rekursion. Dazu gleich mehr.

Pointer-Receiver vs. Value-Receiver

Welcher Typ am Ende tatsächlich Stringer ist, hängt vom Receiver der String()-Methode ab. Bei einem Value-Receiver erfüllen sowohl T als auch *T das Interface. Bei einem Pointer-Receiver ist nur *T ein Stringer.

Die gute Nachricht: fmt ist freundlich. Wenn du einen addressable T-Wert übergibst, nimmt fmt intern via Reflection die Adresse und ruft String() auf *T auf. Bei nicht-adressierbaren Werten — etwa Map-Elementen oder Funktions-Rückgaben direkt im Argument — funktioniert das nicht.

Go receiver_varianten.go
package main

import "fmt"

type Vec struct{ X, Y int }

func (v *Vec) String() string {
	return fmt.Sprintf("(%d, %d)", v.X, v.Y)
}

func main() {
	v := Vec{3, 4}

	fmt.Println(v)
	fmt.Println(&v)

	makeVec := func() Vec { return Vec{7, 8} }
	fmt.Println(makeVec())

	m := map[string]Vec{"a": {1, 2}}
	fmt.Println(m["a"])
}
Output
(3, 4)
(3, 4)
{7 8}
{1 2}

Faustregel: Wenn String() nur liest und der Typ klein ist, ist ein Value-Receiver fast immer die bessere Wahl — er funktioniert in allen Kontexten.

Die Rekursions-Falle

Die meistgemachte Anfängerfehler mit Stringer ist ein Einzeiler, der so harmlos aussieht, dass er regelmäßig übersehen wird: return fmt.Sprintf("%v", t) im String()-Body, wo t der Receiver ist. Was passiert? Sprintf sieht den %v-Verb und einen Stringer-Wert — also ruft es brav String() auf. Diese Methode ruft erneut Sprintf("%v", t). Stack wächst. Stack-Overflow. Programm stirbt.

Go rekursion_bug.go
package main

import "fmt"

type Buggy struct {
	Name string
	Age  int
}

// BUG: %v auf den Receiver → fmt ruft wieder String() auf.
func (b Buggy) String() string {
	return fmt.Sprintf("Buggy %v", b)
}

func main() {
	fmt.Println(Buggy{"Ada", 30})
}

Die saubere Lösung: Greife im String()-Body niemals den ganzen Receiver mit einem Verb ab, das Stringer triggert. Formatiere stattdessen explizit auf den Feldern des Typs (deren Typen keine eigene String()-Methode haben), oder nutze den Type-Alias-Trick.

Go rekursion_fix.go
package main

import "fmt"

type Clean struct {
	Name string
	Age  int
}

func (c Clean) String() string {
	return fmt.Sprintf("Clean{Name: %q, Age: %d}", c.Name, c.Age)
}

func main() {
	fmt.Println(Clean{"Ada", 30})
}
Output
Clean{Name: "Ada", Age: 30}

Der Type-Alias-Trick

Manchmal willst du in String() fast die Default-Repräsentation eines Structs — nur mit etwas Prefix oder Suffix — und es wäre lästig, jedes Feld einzeln aufzulisten. Hier hilft ein Type-Alias ohne Methoden: Ein lokal definierter Typ, der dieselben Felder hat, aber keine String()-Methode trägt. Castest du den Receiver auf diesen Alias, sieht fmt keinen Stringer mehr und fällt auf die Default-Struct-Formatierung zurück.

Go type_alias_trick.go
package main

import "fmt"

type User struct {
	Name string
	Age  int
}

func (u User) String() string {
	type alias User
	return fmt.Sprintf("User%+v", alias(u))
}

func main() {
	fmt.Println(User{"Ada", 30})
	fmt.Println(User{"Linus", 55})
}
Output
User{Name:Ada Age:30}
User{Name:Linus Age:55}

Der Trick funktioniert, weil type alias User einen neuen Named Type mit identischem Underlying-Type erzeugt — und Methoden in Go an Named Types hängen, nicht an Strukturen. alias erbt also keine Methoden von User. fmt sieht einen ganz normalen Struct und formatiert ihn per Reflection.

Beziehung zum error-Interface

Das error-Interface hat eine strukturell identische Signatur wie Stringer: eine Methode, die einen string zurückgibt — nur heißt sie Error() statt String(). Trotz der Ähnlichkeit sind die beiden Interfaces getrennt: Ein Typ mit Error() string ist nicht automatisch Stringer, und umgekehrt. Wenn du beide brauchst, musst du beide Methoden definieren.

Go error_und_stringer.go
package main

import "fmt"

type NotFound struct {
	Resource string
	ID       int
}

func (n NotFound) Error() string {
	return fmt.Sprintf("%s #%d nicht gefunden", n.Resource, n.ID)
}

func (n NotFound) String() string {
	return fmt.Sprintf("NotFound(resource=%q, id=%d)", n.Resource, n.ID)
}

func main() {
	err := NotFound{"User", 42}

	var e error = err
	fmt.Println("err:", e)

	var s fmt.Stringer = err
	fmt.Println("str:", s)
}
Output
err: User #42 nicht gefunden
str: NotFound(resource="User", id=42)

Die Subtilität: fmt priorisiert Error() vor String(), wenn der Wert als Interface vom Typ error daherkommt. Wird derselbe Wert aber als konkreter Typ übergeben, gewinnt String().

GoStringer für %#v, Formatter für volle Kontrolle

Das Verb %#v erzeugt eine Go-Syntax-Repräsentation eines Werts. Wer diese Darstellung anpassen will, implementiert das fmt.GoStringer-Interface mit der Methode GoString() string. In der Praxis ist das selten — die meisten Typen kommen mit der Default-Reflection-Ausgabe gut aus.

Wer pro Verb (%v vs. %+v vs. %s vs. eigene Flags) unterschiedlich formatieren möchte, braucht das fmt.Formatter-Interface mit der Methode Format(f fmt.State, verb rune). Diese Methode bekommt das aktuelle Verb und den Format-State als Argument und schreibt das Ergebnis selbst in den State. Formatter ist mächtiger, aber auch deutlich aufwändiger.

Go gostringer.go
package main

import "fmt"

type Color struct {
	R, G, B uint8
}

func (c Color) String() string {
	return fmt.Sprintf("#%02x%02x%02x", c.R, c.G, c.B)
}

func (c Color) GoString() string {
	return fmt.Sprintf("Color{R: 0x%02x, G: 0x%02x, B: 0x%02x}", c.R, c.G, c.B)
}

func main() {
	c := Color{255, 128, 0}
	fmt.Printf("%s\n", c)
	fmt.Printf("%v\n", c)
	fmt.Printf("%#v\n", c)
}
Output
#ff8000
#ff8000
Color{R: 0xff, G: 0x80, B: 0x00}

Lesbare Statuscodes für Logs und Tests

Go hat keine eigenen Enums; idiomatisch ersetzt man sie durch einen int-basierten Typ und Konstanten via iota. Das Problem: Ohne Stringer taucht der Status in Logs als nackte Zahl auf. Ein 10-Zeilen-Stringer macht aus dem Enum-Pattern eine echte Verbesserung.

Go praxis_status_enum.go
package main

import "fmt"

type OrderStatus int

const (
	OrderPending OrderStatus = iota
	OrderConfirmed
	OrderProcessing
	OrderShipped
	OrderDelivered
	OrderCancelled
)

func (s OrderStatus) String() string {
	switch s {
	case OrderPending:
		return "Pending"
	case OrderConfirmed:
		return "Confirmed"
	case OrderProcessing:
		return "Processing"
	case OrderShipped:
		return "Shipped"
	case OrderDelivered:
		return "Delivered"
	case OrderCancelled:
		return "Cancelled"
	default:
		return fmt.Sprintf("Unknown(%d)", int(s))
	}
}

type Order struct {
	ID     int
	Status OrderStatus
}

func main() {
	orders := []Order{
		{ID: 1001, Status: OrderConfirmed},
		{ID: 1002, Status: OrderShipped},
		{ID: 1003, Status: OrderCancelled},
	}

	for _, o := range orders {
		fmt.Printf("Order #%d%s\n", o.ID, o.Status)
	}

	fmt.Println("Numeric:", int(OrderProcessing))
}
Output
Order #1001 → Confirmed
Order #1002 → Shipped
Order #1003 → Cancelled
Numeric: 2

Die default-Klausel mit int(s) ist wichtig: Sie bricht die potenzielle Rekursion, falls jemand jemals einen Status-Wert außerhalb des definierten Bereichs konstruiert.

Money-Typ — Geldbeträge sauber darstellen

Geld in float64 zu speichern ist ein klassischer Anti-Pattern (Rundungsfehler). Idiomatisch arbeitet man mit einem Money-Typ, der den Betrag in der kleinsten Einheit der Währung als int64 hält und die Währung separat trägt. Stringer macht aus diesem Typ einen lesbaren Wert in Logs, Rechnungen und Test-Output.

Go praxis_money.go
package main

import "fmt"

type Money struct {
	Cents    int64
	Currency string
}

// Formatiert auf den Feldern, nicht auf dem Receiver — keine Rekursion.
func (m Money) String() string {
	major := m.Cents / 100
	minor := m.Cents % 100
	if minor < 0 {
		minor = -minor
	}
	return fmt.Sprintf("%d.%02d %s", major, minor, m.Currency)
}

func (m Money) Add(other Money) Money {
	if m.Currency != other.Currency {
		panic("currency mismatch")
	}
	return Money{Cents: m.Cents + other.Cents, Currency: m.Currency}
}

func main() {
	preis := Money{Cents: 1234, Currency: "EUR"}
	versand := Money{Cents: 499, Currency: "EUR"}
	gesamt := preis.Add(versand)

	fmt.Println("Preis:   ", preis)
	fmt.Println("Versand: ", versand)
	fmt.Println("Gesamt:  ", gesamt)

	fmt.Printf("Rechnung: %s + %s = %s\n", preis, versand, gesamt)
}
Output
Preis:    12.34 EUR
Versand:  4.99 EUR
Gesamt:   17.33 EUR
Rechnung: 12.34 EUR + 4.99 EUR = 17.33 EUR

Der Sprintf-Aufruf in String() formatiert auf den Feldern m.Cents (int64) und m.Currency (string) — beides Typen ohne eigenes String(). Damit ist Rekursion ausgeschlossen, und der Code bleibt lesbar.

Häufige Stolperfallen

Rekursion durch %v auf den Receiver

fmt.Sprintf(&quot;%v&quot;, t) im eigenen String() ist die klassische Falle: fmt ruft String() erneut auf, Stack-Overflow garantiert. Immer auf Feldern formatieren oder Type-Alias verwenden.

Type-Alias als Default-Repr-Workaround

Wenn die Default-Struct-Darstellung gebraucht wird, lokalen Alias type alias T definieren und auf ihn casten — der Alias erbt keine Methoden und wird per Reflection formatiert.

Pointer-Receiver: Nur *T ist Stringer

func (t *T) String() string macht *T zum Stringer, nicht T. Bei nicht-adressierbaren Werten (Map-Elemente, Funktions-Rückgaben) wird String() nicht aufgerufen — Value-Receiver ist im Zweifel sicherer.

%s, %v, %q nutzen Stringer — %d, %x, %T nicht

Stringer greift nur bei Verben, die eine Wert-Beschreibung erwarten. %d, %x, %T, %p adressieren die rohen Daten und ignorieren String().

String() und Error() sind separate Interfaces

Strukturell identisch, aber nicht austauschbar. Wer beides will, implementiert beide Methoden — Error() für Fehler-Pfade, String() für reine Repräsentation.

fmt nimmt Adresse bei addressable Values

Bei String() mit Pointer-Receiver und einer adressierbaren Variable nimmt fmt intern via Reflection die Adresse. Bei Map-Elementen und Funktions-Rückgaben funktioniert das nicht.

GoStringer für %#v, Formatter für volle Kontrolle

GoString() string für die Go-Syntax-Repräsentation (%#v). Format(f State, verb rune) für verb-abhängige Ausgabe. Stringer reicht für 95 % der Fälle.

Stringer macht Enums lesbar

Ein int-Typ mit iota-Konstanten und einer String()-Switch-Methode ist das idiomatische Enum-Pattern in Go — Logs und Test-Output werden sofort menschenlesbar.

Weiterführende Ressourcen

Externe Quellen

/ Weiter

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

Zur Übersicht