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.Sliceprzyjmują 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.