Menu

Domknięcia i funkcje anonimowe w Go (Golang) z przykładami

Funkcje anonimowe w Go mogą przechwytywać zmienne z otaczającego zakresu. Tak powstają domknięcia (closures): liczniki, generatory, middleware i callbacki, które mają własny stan.

Na tej stronie są działające edytory: edytuj, uruchamiaj i od razu zobacz wynik.

Funkcje anonimowe

Literał funkcji to funkcja bez nazwy. Jest wyrażeniem, więc może trafić wszędzie tam, gdzie wartość: do zmiennej, argumentu, wartości zwracanej, pola struktury.

Go nie ma skróconej składni ze strzałką ani lambd. Pełna forma func(params) results { body } to jedyny sposób zapisu.

Przechwytywanie zmiennych

Literał funkcji może używać zmiennych z otaczającej funkcji. Nie kopiuje ich: domknięcie i otaczający kod dzielą jedną zmienną.

Przechwycona zmienna żyje tak długo, jak długo odwołuje się do niej jakieś domknięcie, nawet gdy funkcja, która ją zadeklarowała, już zwróciła wynik. Kompilator w razie potrzeby przenosi ją na stertę; nie musisz tym w ogóle zarządzać.

Generatory i liczniki

Zwrócenie domknięcia z funkcji daje każdej zwróconej funkcji jej własny, prywatny stan.

Każde wywołanie counter() tworzy nowe n, więc c1 i c2 liczą niezależnie. Nic z zewnątrz nie może odczytać ani wyzerować n, co jest małą formą enkapsulacji.

Domknięcia w pętlach (Go 1.22 i nowsze)

Przed Go 1.22 pętla for miała jedną zmienną pętli na całą pętlę. Domknięcia utworzone w jej wnętrzu przechwytywały tę jedną zmienną i widziały jej końcową wartość. To był najczęstszy błąd z domknięciami w Go.

Od Go 1.22 każda iteracja dostaje własną kopię, więc poniższy kod wypisuje 0 1 2:

W Go 1.21 i starszych ta sama pętla zapisana jako for i := 0; i < 3; i++ (range po liczbie całkowitej też jest nowością w 1.22) wypisywała 3 3 3. Nowa reguła działa na poziomie modułu, na podstawie linii go w go.mod, więc stary moduł zachowuje stare zachowanie, dopóki jego linia go nie zostanie podniesiona do 1.22 lub wyżej. W starszym kodzie nadal zobaczysz i := i albo v := v w pętlach: była to ręczna poprawka, dziś zbędna.

Reguła dotycząca iteracji obejmuje tylko zmienne zadeklarowane przez samą instrukcję for. Zmienna zadeklarowana przed pętlą i zmieniana w jej ciele to nadal jedna wspólna zmienna:

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

Domknięcia i gorutyny

Gorutyna uruchomiona z literałem funkcji to też domknięcie. Obowiązują te same zasady współdzielenia, plus zwykła reguła współbieżności: jeśli kilka gorutyn zapisuje przechwyconą zmienną, potrzebujesz mutexa albo kanału.

To zawsze wypisuje 5050. Każda gorutyna czyta własne i (semantyka pętli z Go 1.22) i zapisuje wspólne total pod blokadą. Usuń mutex, a wynik stanie się nieprzewidywalny; go run -race zgłosi to jako wyścig danych. Na stronie o gorutynach znajdziesz WaitGroup i detektor wyścigów.

Gdzie domknięcia pojawiają się w prawdziwym kodzie

  • Sortowanie i wyszukiwanie: slices.SortFunc, slices.IndexFunc, sort.Slice przyjmują literał funkcji.
  • Odroczone sprzątanie: defer func() { ... }() uruchamia blok przy wyjściu z funkcji i może czytać jej zmienne. Zobacz defer.
  • Middleware HTTP: funkcja, która przyjmuje handler i zwraca nowy literał http.HandlerFunc, który go opakowuje.
  • Konfiguracja: opcje funkcyjne (func WithTimeout(d time.Duration) Option { return func(c *Config) { c.Timeout = d } }) to domknięcia nad argumentem.

Przykład middleware, który przechwytuje zarówno parametr, jak i licznik:

Typowe błędy

  • Oczekiwanie migawki. Domknięcie czyta bieżącą wartość zmiennej w chwili uruchomienia, a nie wartość z chwili utworzenia domknięcia. Jeśli potrzebujesz migawki, przekaż wartość jako parametr: go func(v int) { ... }(x).
  • Rekurencyjne literały. Literał funkcji nie może odwołać się do siebie przez zmienną, do której jest przypisywany w tym samym :=. Najpierw zadeklaruj zmienną: var walk func(n int); walk = func(n int) { ... walk(n-1) }.
  • Niesynchronizowane zapisy z gorutyn. Przechwytywanie to nie synchronizacja. Chroń wspólne zapisy.

Najczęściej zadawane pytania

Czym jest funkcja anonimowa w Go?

To literał funkcji bez nazwy: func(x int) int { return x * 2 }. Możesz przypisać go do zmiennej, przekazać jako argument, zwrócić albo wywołać od razu, dopisując () po zamykającym nawiasie klamrowym. Go nie ma osobnej składni lambd; literał funkcji to właśnie lambda.

Czym jest domknięcie (closure) w Go?

To literał funkcji, który odwołuje się do zmiennych zadeklarowanych poza nim. Funkcja utrzymuje te zmienne przy życiu i dzieli je z otaczającym zakresem, więc zmiany wprowadzone w domknięciu są widoczne na zewnątrz i odwrotnie. Zmienne są przechwytywane przez referencję, a nie kopiowane.

Czy domknięcia w Go przechwytują zmienne przez wartość, czy przez referencję?

Przez referencję. Domknięcie i otaczający kod używają tej samej zmiennej. Jeśli potrzebujesz migawki, skopiuj wartość do nowej zmiennej przed utworzeniem domknięcia albo przekaż ją jako argument.

Czy błąd ze zmienną pętli w domknięciach został naprawiony w Go?

Tak, od Go 1.22. Każda iteracja pętli for deklaruje teraz nową zmienną pętli, więc domknięcia i gorutyny utworzone w pętli widzą wartość z tej iteracji. Dotyczy to modułów, których go.mod zawiera go 1.22 lub nowszą wersję. Starszy kod często ma w pętli obejście i := i; jest nieszkodliwe, ale już niepotrzebne.

Ilustracja języków programowania w Coddy

Ucz się programowania z Coddy

ZACZNIJ