Menu

Golang Closures und anonyme Funktionen, mit Beispielen

Anonyme Funktionen in Go können Variablen aus dem umgebenden Scope einfangen. Daraus entstehen Closures: Zähler, Generatoren, Middleware und Callbacks mit eigenem Zustand.

Diese Seite enthält ausführbare Editoren - bearbeiten, ausführen und Ausgabe sofort sehen.

Anonyme Funktionen

Ein Funktionsliteral ist eine Funktion ohne Namen. Es ist ein Ausdruck und kann überall stehen, wo ein Wert stehen kann: in einer Variablen, als Argument, als Rückgabewert, in einem Struct-Feld.

Go hat keine Pfeil- oder Lambda-Kurzschreibweise. Die vollständige Form func(params) results { body } ist der einzige Weg.

Variablen einfangen

Ein Funktionsliteral kann Variablen der umgebenden Funktion benutzen. Es kopiert sie nicht: Closure und umgebender Code teilen sich eine Variable.

Die eingefangene Variable lebt, solange noch eine Closure auf sie verweist, auch nachdem die Funktion, die sie deklariert hat, zurückgekehrt ist. Der Compiler verschiebt sie bei Bedarf auf den Heap; darum musst du dich nie kümmern.

Generatoren und Zähler

Gibt eine Funktion eine Closure zurück, bekommt jede zurückgegebene Funktion ihren eigenen privaten Zustand.

Jeder Aufruf von counter() erzeugt ein neues n, also zählen c1 und c2 unabhängig voneinander. Von außen kann niemand n lesen oder zurücksetzen. Das ist eine kleine Form der Kapselung.

Closures in Schleifen (ab Go 1.22)

Vor Go 1.22 hatte eine for-Schleife eine einzige Schleifenvariable für die ganze Schleife. Alle Closures, die darin entstanden, fingen diese eine Variable ein und sahen ihren Endwert. Das war der häufigste Closure-Bug in Go.

Seit Go 1.22 bekommt jeder Durchlauf seine eigene Kopie, also gibt der Code unten 0 1 2 aus:

Unter Go 1.21 und älter gab dieselbe Schleife, geschrieben als for i := 0; i < 3; i++ (range über eine Ganzzahl ist ebenfalls neu in 1.22), 3 3 3 aus. Die neue Regel gilt pro Modul, abhängig von der go-Zeile in go.mod, also behält ein altes Modul das alte Verhalten, bis seine go-Zeile auf 1.22 oder höher angehoben wird. In älterem Code siehst du noch i := i oder v := v in Schleifen: Das war der manuelle Fix und ist jetzt überflüssig.

Die Regel pro Durchlauf gilt nur für Variablen, die die for-Anweisung selbst deklariert. Eine Variable, die vor der Schleife deklariert und im Rumpf aktualisiert wird, bleibt eine einzige geteilte Variable:

var last string
for _, s := range items {
	last = s
	handlers = append(handlers, func() { use(last) }) // every closure sees the final value
}

Closures und Goroutinen

Eine Goroutine, die mit einem Funktionsliteral gestartet wird, ist ebenfalls eine Closure. Es gelten dieselben Regeln fürs Teilen, dazu die übliche Regel für Nebenläufigkeit: Wenn mehrere Goroutinen eine eingefangene Variable schreiben, brauchst du einen Mutex oder einen Channel.

Das gibt immer 5050 aus. Jede Goroutine liest ihr eigenes i (Schleifensemantik ab Go 1.22) und schreibt das geteilte total unter einem Lock. Entfernst du den Mutex, wird das Ergebnis unvorhersehbar; go run -race meldet ein Data Race. Zu WaitGroup und dem Race Detector siehe Goroutinen.

Wo Closures in echtem Code auftauchen

  • Sortieren und Suchen: slices.SortFunc, slices.IndexFunc und sort.Slice nehmen alle ein Funktionsliteral.
  • Aufräumen mit defer: defer func() { ... }() führt beim Verlassen der Funktion einen Block aus und kann die Variablen der Funktion lesen. Siehe defer.
  • HTTP-Middleware: eine Funktion, die einen Handler nimmt und ein neues http.HandlerFunc-Literal zurückgibt, das ihn umhüllt.
  • Konfiguration: Functional Options (func WithTimeout(d time.Duration) Option { return func(c *Config) { c.Timeout = d } }) sind Closures über das Argument.

Ein Middleware-Beispiel, das sowohl einen Parameter als auch einen Zähler einfängt:

Häufige Fehler

  • Einen Schnappschuss erwarten. Eine Closure liest den aktuellen Wert der Variable, wenn sie läuft, nicht den Wert beim Erzeugen. Brauchst du einen Schnappschuss, übergib den Wert als Parameter: go func(v int) { ... }(x).
  • Rekursive Literale. Ein Funktionsliteral kann sich nicht über die Variable selbst aufrufen, der es im selben := zugewiesen wird. Deklarier die Variable zuerst: var walk func(n int); walk = func(n int) { ... walk(n-1) }.
  • Unsynchronisierte Schreibzugriffe aus Goroutinen. Einfangen ist keine Synchronisation. Schütze geteilte Schreibzugriffe.

Häufig gestellte Fragen

Was ist eine anonyme Funktion in Go?

Ein Funktionsliteral ohne Namen: func(x int) int { return x * 2 }. Du kannst es einer Variablen zuweisen, als Argument übergeben, zurückgeben oder sofort aufrufen, indem du () hinter die schließende Klammer setzt. Go hat keine eigene Lambda-Syntax; das Funktionsliteral ist das Lambda.

Was ist eine Closure in Go?

Ein Funktionsliteral, das auf Variablen zugreift, die außerhalb davon deklariert sind. Die Funktion hält diese Variablen am Leben und teilt sie mit dem umgebenden Scope, also sind Änderungen innerhalb der Closure außen sichtbar und umgekehrt. Variablen werden per Referenz eingefangen, nicht kopiert.

Fangen Go-Closures Variablen als Wert oder als Referenz ein?

Als Referenz. Die Closure und der umgebende Code verwenden dieselbe Variable. Brauchst du einen Schnappschuss, kopier den Wert vor dem Erzeugen der Closure in eine neue Variable oder übergib ihn als Argument.

Ist der Closure-Bug mit der Schleifenvariable in Go behoben?

Ja, seit Go 1.22. Jeder Durchlauf einer for-Schleife deklariert jetzt eine neue Schleifenvariable, also sehen Closures und Goroutinen, die in der Schleife entstehen, den Wert ihres Durchlaufs. Das gilt für Module, deren go.mod go 1.22 oder höher angibt. Älterer Code enthält oft i := i in der Schleife als Workaround; das schadet nicht, ist aber nicht mehr nötig.

Coddy programming languages illustration

Lerne mit Coddy zu programmieren

LOS GEHT'S