Skip to main content
Entwickler Themen
Toggle Dark/Light/Auto mode Toggle Dark/Light/Auto mode Toggle Dark/Light/Auto mode Back to homepage

7.2.3. Funktionen und Fehler

Funktionen in Go liefern oft mehrere Werte; Fehler sind der letzte davon. Der Happy Path bleibt flach, der Fehlerpfad wird eingerückt.

Mehrfachrückgabe statt In-Band-Fehler

func Lookup(key string) (value string, ok bool)
func (f *File) Write(b []byte) (n int, err error)

error oder ok stehen zuletzt. Keine „magischen“ Rückgabewerte als Ersatz für Fehler.

Error-Pfad einrücken

x, err := f()
if err != nil {
	return err
}
// Happy Path ohne Extra-Einrückung

Named Results sparsam

Benannte Rückgaben helfen, wenn mehrere Ergebnisse denselben Typ haben oder ein defer den Return-Wert setzt. Nur um nacktes return zu ermöglichen, lohnen sie in längeren Funktionen selten.

Synchron bevorzugen

Eine Funktion sollte ihre Arbeit erledigen (oder Kanäle/Werte zurückgeben), bevor sie zurückkehrt. Braucht der Caller Parallelität, startet er selbst go f().

Receiver: Wert oder Pointer

Situation Bevorzugt
Methode verändert den Receiver Pointer
Struct enthält sync.Mutex Pointer
Großes Struct Pointer
Map / func / chan Wert
Kleiner unveränderlicher / Basis-Typ oft Wert
Unsicher Pointer

Pointer- und Value-Receiver auf demselben Typ nicht ohne Grund mischen. Ein Mutex per Wert zu kopieren ist ein copylocks-Bug.

func (c *Counter) Inc() {
	c.mu.Lock()
	c.n++
	c.mu.Unlock()
}

string und io.Reader als Pointer zu übergeben, nur um Bytes zu sparen, ist meist Rauschen – Werte reichen.

Generische Methoden

Ab Go 1.27 darf eine Methode eigene Typparameter haben, wenn die Operation zum Typ gehört. Interface-Methoden dürfen keine Typparameter deklarieren; eine generische Methode implementiert keine Interface-Methode.

Fehler prüfen – und nicht wegwerfen

f, err := os.Open(name)
if err != nil {
	return fmt.Errorf("open %s: %w", name, err)
}
defer f.Close()

_ für Fehler nur mit Kommentar, der begründet, warum das Absicht ist.

Fehlertexte

Klein geschrieben, ohne Satzzeichen am Ende – sie werden in größere Meldungen eingebettet. Log-Zeilen sind die Ausnahme (zeilenorientiert).

fmt.Errorf("something bad")            // gut
fmt.Errorf("Something bad.")           // vermeiden
log.Printf("Reading %s: %v", name, err)

Wrapping: %w, errors.Is, errors.AsType

if err != nil {
	return fmt.Errorf("decompress %s: %w", name, err)
}

if errors.Is(err, ErrNotFound) { /* ... */ }

if qe, ok := errors.AsType[*QueryError](err); ok { /* ... */ }

%w macht die Ursache für Is / AsType sichtbar. errors.Is statt ==; errors.AsType statt Type-Assert, der Wrapping übersieht. %v nur, wenn die Ursache bewusst versteckt werden soll.

Mehrere unabhängige Fehler: errors.Join(errA, errB) – nil-Argumente fallen weg.

Sentinel vs. eigener Fehlertyp

Bedarf Typisch
Kein Matching, fester Text errors.New("…")
Kein Matching, dynamischer Text fmt.Errorf("…")
Caller matcht feste Bedingung exportiertes var ErrX = errors.New("…") + errors.Is
Caller braucht Struktur eigener Typ mit Error() (+ Unwrap) + AsType
var ErrNotFound = errors.New("not found")

type QueryError struct {
	Query string
	Err   error
}

func (e *QueryError) Error() string {
	return e.Query + ": " + e.Err.Error()
}
func (e *QueryError) Unwrap() error { return e.Err }

Fehler einmal behandeln

Nicht in jeder Schicht loggen und denselben Fehler weiterreichen. Entweder wrap+return, oder loggen und absorbieren, wenn man graceful degradiert – eine Verantwortung pro Schicht (verbreitete Praxis; bei Uber explizit).

Kein panic für normalen Kontrollfluss

Gewöhnliche Fehler → error. panic nur für wirklich Unmögliches / Unwiederbringliches (oder bewusst recovered Boundaries). In Libraries: lieber error zurückgeben; Must… höchstens bei Init.

Diese Seite gehört zu 7.2. Clean Code.