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.
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.
x, err := f()
if err != nil {
return err
}
// Happy Path ohne Extra-Einrückung
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.
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().
| 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.
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.
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.
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)
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.
| 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 }
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).
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.