Funktionale Programmierung in Go: Vom Imperativen zum Deklarativen
Go (Golang) wird traditionell mit imperativer und Concurrent-Programmierung assoziiert, aber sein mächtiges Typsystem und die Unterstützung von Funktionen erster Klasse machen die Anwendung vieler Prinzipien der funktionalen Programmierung (FP) möglich. In diesem Artikel werden wir untersuchen, wie man FP-Idiome in Go anwendet, ohne Einbußen bei der Leistung und Lesbarkeit des Codes hinzunehmen.
Grundprinzipien der FP im Kontext von Go
Funktionale Programmierung in Go bedeutet nicht den vollständigen Verzicht auf veränderlichen Zustand. Stattdessen verwenden wir die folgenden Konzepte:
- Reine Funktionen — Funktionen, deren Ergebnis nur von den Argumenten abhängt, ohne Seiteneffekte.
- Unveränderlichkeit (Immutability) — Bevorzugung der Erstellung neuer Datenstrukturen anstelle der Änderung bestehender.
- Funktionen höherer Ordnung — Funktionen, die andere Funktionen als Argumente akzeptieren oder sie zurückgeben.
- Closures — Funktionen, die Variablen aus dem äußeren Gültigkeitsbereich einfangen.
Funktionen höherer Ordnung: map, filter, reduce
Go hat keine eingebauten Generics (bis Version 1.18), daher werden die klassischen map, filter, reduce über Interfaces oder explizites Kopieren implementiert. Betrachten wir ein Beispiel mit ganzen Zahlen:
package main
import "fmt"
// Map — wendet eine Funktion auf jedes Element eines Slice anfunc Map(s []int, fn func(int) int) []int { result := make([]int, len(s)) for i, v := range s { result[i] = fn(v) } return result}
// Filter — behält nur Elemente, die die Bedingung erfüllenfunc Filter(s []int, fn func(int) bool) []int { var result []int for _, v := range s { if fn(v) { result = append(result, v) } } return result}
// Reduce — faltet einen Slice zu einem einzelnen Wertfunc Reduce(s []int, init int, fn func(int, int) int) int { acc := init for _, v := range s { acc = fn(acc, v) } return acc}
func main() { nums := []int{1, 2, 3, 4, 5}
// Verdoppelt jedes Element doubled := Map(nums, func(x int) int { return x * 2 }) fmt.Println("Map:", doubled) // [2 4 6 8 10]
// Behält nur gerade Zahlen evens := Filter(nums, func(x int) bool { return x%2 == 0 }) fmt.Println("Filter:", evens) // [2 4]
// Summiert alle Elemente sum := Reduce(nums, 0, func(acc, x int) int { return acc + x }) fmt.Println("Reduce:", sum) // 15}Wichtig: In Go 1.18+ mit Generics können universelle Versionen erstellt werden:
// Generic Mapfunc MapGeneric[T, U any](s []T, fn func(T) U) []U { result := make([]U, len(s)) for i, v := range s { result[i] = fn(v) } return result}Closures und Currying in Go
Closures in Go sind ein mächtiges Werkzeug zum Erstellen von Funktionen mit Zustandserhaltung. Betrachten wir ein Beispiel eines Sequenzgenerators:
package main
import "fmt"
// Fibonacci-Sequenzgeneratorfunc fibonacci() func() int { a, b := 0, 1 return func() int { a, b = b, a+b return a }}
// Currying: eine Funktion, die Argumente schrittweise annimmtfunc add(a int) func(int) int { return func(b int) int { return a + b }}
func main() { fib := fibonacci() fmt.Println(fib()) // 1 fmt.Println(fib()) // 1 fmt.Println(fib()) // 2
// Currying add5 := add(5) fmt.Println(add5(10)) // 15 fmt.Println(add5(20)) // 25}Closures sind besonders nützlich für:
- Erstellung konfigurierbarer Funktionen (Middleware)
- Implementierung des "Funktionsfabrik"-Musters
- Organisation von Callback-Mechanismen