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
- 2. Vergessen, das Ergebnis von append zuzuweisen
- 3. Nullwerte, die fehlende Map-Keys verbergen
- 4. %v statt %w beim Wrappen von Fehlern
- 5. Value Receiver, die Änderungen stillschweigend verwerfen
- 6. Verschattete Variablen, die nil-Fehler zurückgeben
- 7. Nicht exportierte Struct-Felder verschwinden im JSON
- 8. defer in Schleifen verwenden
- 9. Fehler mit dem Blank Identifier ignorieren
- 10. Deadlocks durch ungepufferte Channels
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.gogopackage 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.gogopackage 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.gogopackage 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.gogopackage 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.gogopackage 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.gogofunc 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.gogopackage 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.gogofunc 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.gogofunc 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.gogodata, _ := 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.gogopackage 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.gogopackage 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.
