Zurück zum Blog

10 häufige Fehler in Go (Golang), die du vermeiden solltest

Lerne die 10 häufigsten Fehler beim Programmieren in Go (Golang) kennen und wie du sie vermeidest: Bytes in Strings, append bei Slices, Nullwerte in Maps, Wrapping von Fehlern, Value Receiver, defer in Schleifen, ungepufferte Channels und mehr.

10 häufige Fehler in Go (Golang), die du vermeiden solltest

Go, oft auch als Golang gesucht, ist eine kleine Sprache, und genau deshalb unterschätzt man sie leicht. Selbst Entwickler mit jahrelanger Go-Erfahrung tappen immer wieder in dieselben paar Fallstricke. Die meisten dieser Bugs kompilieren problemlos. Das Programm liefert einfach das falsche Ergebnis, verursacht Ressourcenlecks oder endet in Produktion in einem Deadlock.

Hier sind die 10 häufigsten davon. Zu jedem gibt es ein lauffähiges Golang-Beispiel und die idiomatische Lösung.

Inhaltsverzeichnis


1. Strings als Zeichen statt als Bytes behandeln

Ein Go-String ist ein schreibgeschützter Slice aus Bytes, nicht aus Zeichen. len(s) liefert die Anzahl der Bytes, und der Indexzugriff mit s[i] liefert ein Byte. Für ASCII-Text ist das kein Problem. Es geht kaputt, sobald ein Zeichen mehr als ein Byte belegt.

example.gogo
package main

import (
    "fmt"
    "unicode/utf8"
)

func main() {
    word := "kött"
    fmt.Println("len:", len(word))
    fmt.Println("runes:", utf8.RuneCountInString(word))
}

Das schwedische Wort für Fleisch hat vier Zeichen, aber fünf Bytes. Wenn du per Byte-Index slicest, kannst du eine Rune in der Mitte durchtrennen und ungültiges UTF-8 erzeugen.

Lösung: Zähle Zeichen mit utf8.RuneCountInString. Beim Iterieren liefert dir for i, r := range s Runes. Zum Slicen wandelst du den String vorher in []rune um.

2. Vergessen, das Ergebnis von append zuzuweisen

append aktualisiert den Slice nicht, den du übergibst. Reicht die Kapazität nicht aus, legt es ein neues zugrunde liegendes Array an und gibt einen Slice zurück, der darauf zeigt. Ignorierst du den Rückgabewert, gehen die angehängten Elemente verloren.

example.gogo
package main

import "fmt"

func main() {
    nums := []int{1, 2, 3}
    appendWrong(nums)
    fmt.Println("wrong:", nums)

    nums = appendRight(nums)
    fmt.Println("right:", nums)
}

func appendWrong(s []int) {
    _ = append(s, 4)
}

func appendRight(s []int) []int {
    return append(s, 4)
}

Ein nackter Aufruf append(s, 4) ist in Go tatsächlich ein Compilerfehler, weil der Rückgabewert verwendet werden muss. Das _ = in appendWrong steht nur dort, damit der Compiler es durchlässt. Es verhält sich genauso, als würdest du append aufrufen und das Ergebnis nie wieder zuweisen.

Lösung: Weise immer neu zu, also s = append(s, x). Wenn eine Hilfsfunktion einen Slice vergrößert, lass sie den neuen Slice zurückgeben und weise ihn an der Aufrufstelle neu zu.

3. Nullwerte, die fehlende Map-Keys verbergen

Liest du einen fehlenden Key aus einer Map, bekommst du keinen Fehler. Du bekommst den Nullwert (Zero Value) des Werttyps der Map zurück. Bei einer map[string]int ist das 0, und das kannst du nicht von einem Key unterscheiden, der absichtlich auf null gesetzt wurde.

example.gogo
package main

import "fmt"

func main() {
    scores := map[string]int{"alice": 0}

    if v := scores["alice"]; v == 0 {
        fmt.Println("alice looks the same as missing users")
    }

    if v, ok := scores["bob"]; ok {
        fmt.Println("bob:", v)
    } else {
        fmt.Println("bob is missing")
    }
}

Lösung: Nutze das Comma-ok-Idiom, v, ok := m[key]. Der boolesche Wert sagt dir, ob der Key existiert hat.

4. %v statt %w beim Wrappen von Fehlern

fmt.Errorf mit %v formatiert den ursprünglichen Fehler in einen neuen Text und wirft den Fehlerwert weg. Mit %w wrappt der neue Fehler das Original, sodass errors.Is und errors.As es weiterhin finden.

example.gogo
package main

import (
    "errors"
    "fmt"
    "os"
)

func main() {
    _, err := os.Open("does-not-exist.txt")
    wrapped := fmt.Errorf("reading config: %w", err)

    fmt.Println(wrapped)
    fmt.Println("is not-exist:", errors.Is(wrapped, os.ErrNotExist))
}

Lösung: Wrappe mit %w, wann immer Aufrufer den zugrunde liegenden Fehler prüfen müssen könnten. Verwende %v nur dann, wenn der ursprüngliche Fehler ein Implementierungsdetail ist, von dem Aufrufer nicht abhängen sollen.

5. Value Receiver, die Änderungen stillschweigend verwerfen

Eine Methode mit Value Receiver bekommt eine Kopie des Structs. Was sie an dieser Kopie ändert, ist weg, sobald die Methode zurückkehrt.

example.gogo
package main

import "fmt"

type Counter struct {
    n int
}

func (c Counter) IncValue() {
    c.n++
}

func (c *Counter) IncPointer() {
    c.n++
}

func main() {
    c := Counter{}
    c.IncValue()
    fmt.Println("after value receiver:", c.n)

    c.IncPointer()
    fmt.Println("after pointer receiver:", c.n)
}

Lösung: Verwende einen Pointer Receiver, wenn die Methode den Receiver verändert oder das Struct so groß ist, dass Kopieren Verschwendung wäre. Brauchen einige Methoden eines Typs einen Pointer Receiver, ist es meist sauberer, allen einen zu geben.

6. Verschattete Variablen, die nil-Fehler zurückgeben

Eine kurze Variablendeklaration (:=) innerhalb eines if erzeugt eine neue Variable, die nur in diesem Block existiert. Verschattet sie versehentlich ein äußeres err (Shadowing), bleibt das äußere nil und die Funktion meldet Erfolg, obwohl etwas fehlgeschlagen ist.

example.gogo
func loadUser(id string) (*User, error) {
    var err error
    user, err := fetch(id)
    if err != nil {
        return nil, err
    }

    if err := validate(user); err != nil {
        log.Println(err)
        // the outer err is still nil here
    }

    return user, err
}

Lösung: Weise der äußeren Variable mit = statt := zu oder kehre direkt im if-Block zurück. Aktiviere go vet -shadow oder den govet-Linter, damit solche Fälle schon vor dem Code Review auffallen.

7. Nicht exportierte Struct-Felder verschwinden im JSON

encoding/json sieht nur exportierte Felder. Ein kleingeschriebenes Feld ist für den Marshaller unsichtbar und fehlt deshalb ohne jede Fehlermeldung in der Ausgabe.

example.gogo
package main

import (
    "encoding/json"
    "fmt"
)

type User struct {
    Name  string `json:"name"`
    email string
    Age   int    `json:"age,omitempty"`
}

func main() {
    u := User{Name: "Ada", email: "[email protected]", Age: 36}
    data, _ := json.Marshal(u)
    fmt.Println(string(data))
}

Die Ausgabe enthält keinen email-Key, und nichts warnt dich davor.

Lösung: Schreib die Felder, die serialisiert werden sollen, groß und lege den Namen des JSON-Keys über Struct-Tags fest. Ergänze omitempty bei optionalen Feldern.

8. defer in Schleifen verwenden

defer läuft, wenn die umgebende Funktion zurückkehrt, nicht am Ende jedes Schleifendurchlaufs. Öffnet eine Schleife tausend Dateien und schiebt für jede file.Close() per defer auf, bleiben alle tausend Dateideskriptoren offen, bis die Funktion beendet ist.

example.gogo
func processAll(paths []string) error {
    for _, p := range paths {
        f, err := os.Open(p)
        if err != nil {
            return err
        }
        defer f.Close() // all closes pile up until processAll returns
        // ... read file ...
    }
    return nil
}

Lösung: Verschiebe die Arbeit pro Durchlauf in eine Hilfsfunktion, damit defer am Ende jedes Aufrufs läuft.

example.gogo
func processAll(paths []string) error {
    for _, p := range paths {
        if err := processOne(p); err != nil {
            return err
        }
    }
    return nil
}

func processOne(path string) error {
    f, err := os.Open(path)
    if err != nil {
        return err
    }
    defer f.Close()
    // ... read file ...
    return nil
}

9. Fehler mit dem Blank Identifier ignorieren

result, _ := doThing() wirft jeden Fehler weg, den die Funktion meldet. Das Programm läuft mit einem Nullwert oder halb fertigem Zustand weiter, und wenn es schließlich abstürzt, dann irgendwo weit entfernt von der eigentlichen Ursache.

example.gogo
data, _ := os.ReadFile("config.json")
_ = json.Unmarshal(data, &cfg)

Fehlt die Datei, ist data gleich nil, Unmarshal gibt einen Fehler zurück, den niemand liest, und cfg bleibt auf dem Nullwert. Du merkst es erst später, wenn sich die App so verhält, als gäbe es gar keine Konfiguration.

Lösung: Behandle den Fehler oder gib ihn an den Aufrufer zurück. Verwende _ nur, wenn der Fehler wirklich keine Rolle spielen kann, und erkläre in einem kurzen Kommentar, warum.

10. Deadlocks durch ungepufferte Channels

Bei einem ungepufferten Channel ist ein Senden erst abgeschlossen, wenn ein Empfänger den Wert im selben Moment entgegennimmt. Empfängt nie jemand, blockiert das Senden für immer.

example.gogo
package main

import "fmt"

func main() {
    ch := make(chan int) // unbuffered
    ch <- 42             // deadlock: no goroutine is receiving
    fmt.Println(<-ch)
}

Das Senden wartet auf einen Empfänger, aber das Empfangen steht in der nächsten Zeile und kommt nie zum Zug. Die Runtime erkennt, dass jede Goroutine hängt, und beendet das Programm mit fatal error: all goroutines are asleep - deadlock!.

Lösung: Sorge dafür, dass eine Goroutine zum Empfangen bereit ist, bevor du sendest, oder nutze einen gepufferten Channel, wenn Producer und Consumer unterschiedlich schnell arbeiten. Für ein einzelnes Ergebnis ist ein Channel mit einem Puffer von 1 ein sicherer Standard.

example.gogo
package main

import "fmt"

func main() {
    ch := make(chan int, 1) // buffered
    ch <- 42
    fmt.Println(<-ch)
}

Wie es weitergeht

Wenn du diese zehn Golang-Fallen kennst, ersparst du dir viel Debugging. Den Rest lernst du, indem du echte Projekte baust und mit offenem Debugger auf Grenzfälle stößt. Für strukturiertes Üben deckt der Go-Lernpfad von LevelUpGo diese Muster mit lauffähigen Übungen und automatischem Feedback zu deinem Code ab.

Schreib Go wie ein Senior Engineer

Interaktive Lektionen im Browser. Die ersten sind kostenlos.

Kostenlose Lektion testenOder kostenloses Konto erstellen