Was macht Go: Idiomatische Effizienzreferenz?
Inhaltsverzeichnis
- Fehlerbehandlung
- Slices & Maps
- Goroutines & Channels
- Structs & Interfaces
- Funktionen & Closures
- Anti-Patterns spezifisch für Go
1. Fehlerbehandlung {#fehler}
// ❌ Fehler ignorieren
result, _ := os.Open(path)
// ✅ — immer behandeln; _ nur verwenden, wenn der Fehler nachweislich irrelevant ist
result, err := os.Open(path)
if err != nil {
return fmt.Errorf("open %s: %w", path, err)
}
// ❌ Redundante Fehlervariable
err := doA()
if err != nil { return err }
err = doB()
if err != nil { return err }
// ✅ — jedes :=/: ist in Ordnung; das ist idiomatisches Go. Versuche nicht, es zu "verbessern".
// Was DU vereinfachen kannst: Zusammenfassen zu Einzeilern, wenn der if-Body ein einzelnes return ist
if err := doA(); err != nil { return err }
if err := doB(); err != nil { return err }
// ❌ Benutzerdefinierter Fehlertyp ohne Mehrwert
type MyError struct{ msg string }
func (e MyError) Error() string { return e.msg }
// ✅ — errors.New oder fmt.Errorf verwenden, es sei denn, Aufrufer müssen den Typ inspizieren
var ErrNotFound = errors.New("not found")
return fmt.Errorf("lookup %q: %w", key, ErrNotFound)
Fehler mit %w (nicht %v) verpacken, damit Aufrufer errors.Is / errors.As verwenden können.
2. Slices & Maps {#slices}
// ❌ Wachsen eines Slice ohne Vorabzuweisung, wenn die Größe bekannt ist
var result []string
for _, item := range items {
result = append(result, item.Name)
}
// ✅
result := make([]string, 0, len(items))
for _, item := range items {
result = append(result, item.Name)
}
// ❌ Manuelle Existenzprüfung vor Map-Schreibzugriff
if _, ok := m[key]; !ok {
m[key] = []string{}
}
m[key] = append(m[key], value)
// ✅ — Anhängen an nil-Slice ist gültiges Go
m[key] = append(m[key], value)
// ❌ Kopieren einer Map durch Zuweisung (kopiert Referenz)
copy := original
// ✅
copy := make(map[K]V, len(original))
for k, v := range original { copy[k] = v }
3. Goroutines & Channels {#concurrency}
// ❌ Feuer-und-Vergessen-Goroutine ohne Lebenszyklus
go doWork()
// ✅ — errgroup oder WaitGroup verwenden, um die Fertigstellung zu verfolgen
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
doWork()
}()
wg.Wait()
// ❌ Ungepufferter Channel verursacht unnötigen Goroutine-Block
ch := make(chan Result)
go func() { ch <- compute() }()
result := <-ch
// ✅ — für Einzelergebnisse vermeidet gepufferter Channel Goroutine-Leaks, wenn der Empfänger vorzeitig beendet
ch := make(chan Result, 1)
go func() { ch <- compute() }()
result := <-ch
// ❌ select mit busy-wait default
for {
select {
case v := <-ch:
process(v)
default:
// spin
}
}
// ✅ — blockierendes select, es sei denn, du benötigst wirklich nicht-blockierendes Verhalten
for v := range ch {
process(v)
}
Verwende golang.org/x/sync/errgroup für Fan-Out mit Fehlersammlung.
4. Structs & Interfaces {#structs}
// ❌ Großes Interface
type Storage interface {
Get(key string) ([]byte, error)
Set(key string, val []byte) error
Delete(key string) error
List(prefix string) ([]string, error)
// ... 10 weitere Methoden
}
// ✅ — kleine, zusammensetzbare Interfaces
type Getter interface { Get(key string) ([]byte, error) }
type Setter interface { Set(key string, val []byte) error }
type Storage interface { Getter; Setter }
// ❌ Konkreten Struct aus Konstruktor zurückgeben (bindet Aufrufer an Implementierung)
func NewStore() *RedisStore { ... }
// ✅ — Interface zurückgeben, wenn es mehrere Implementierungen gibt oder zu erwarten sind
func NewStore() Storage { return &RedisStore{...} }
// ❌ Pointer-Empfänger für kleine Werttypen
func (p *Point) X() float64 { return p.x }
// ✅ — Wert-Empfänger für kleine unveränderliche Typen
func (p Point) X() float64 { return p.x }
Regel: Pointer-Empfänger, wenn die Methode den Zustand ändert ODER der Struct groß ist (>3 Felder von nicht-trivialer Größe). Sonst Wert-Empfänger.
5. Funktionen & Closures {#funktionen}
// ❌ Benannte Rückgabewerte nur verwendet, um eine Variablendeklaration zu vermeiden
func divide(a, b float64) (result float64, err error) {
result = a / b
return
}
// ✅ — benannte Rückgaben lohnen sich nur für verzögerte Mutation oder Dokumentation
func divide(a, b float64) (float64, error) {
if b == 0 { return 0, errors.New("division by zero") }
return a / b, nil
}
// ❌ Closure fängt Schleifenvariable ein (klassischer Go-Bug, behoben in Go 1.22+)
// Vor 1.22: Jede Goroutine fängt dasselbe i ein
for i := 0; i < n; i++ {
go func() { use(i) }()
}
// ✅ (Go <1.22 — als Parameter übergeben)
for i := 0; i < n; i++ {
go func(i int) { use(i) }(i)
}
// Go 1.22+: Schleifenvariable ist pro Iteration gültig, daher ist das Original sicher
6. Anti-Patterns spezifisch für Go {#antipatterns}
| Anti-Pattern | Bevorzugt |
|---|---|
if err != nil { return err } 5+ mal wiederholt | akzeptabel — es ist idiomatisches Go |
panic für erwartete Fehler | return err |
init() mit Seiteneffekten | explizite Initialisierung in main oder Konstruktoren |
interface{} / any ohne Generics | Generics (Go 1.18+) oder typisierte Interfaces verwenden |
| Mutex-Feld nicht neben den Daten, die es schützt | mu direkt über dem Feld platzieren, das es bewacht |
| Channel von Channels | meist ein Zeichen von Over-Engineering; umgestalten |
time.Sleep in Tests | testing-Hooks oder Channels zur Synchronisation verwenden |
| Exportierte Typen mit unexportierten Feldern (wenn die Felder der ganze Sinn sind) | record-artige Structs mit ausschließlich exportierten Feldern |
log.Fatal außerhalb von main | Fehler nach oben zurückgeben |
Einschränkungen
- Dies sind sprachspezifische Richtlinien und decken keine allgemeinen Architekturentscheidungen ab.
- Übermäßige Verdichtung kann die Lesbarkeit beeinträchtigen; mit Bedacht anwenden.