programowanieStarszy programista Go

Opowiedz, jak Go implementuje zamknięcia (literały funkcji/zamknięcia) oraz jakie są ograniczenia i cechy używania zamknięć: gdzie są przechowywane, jak są przechwytywane zmienne, jakie są różnice w zachowaniu przechwyconych zmiennych w różnych scenariuszach?

Zdaj rozmowy kwalifikacyjne z asystentem AI Hintsage

Odpowiedź

W Go funkcje anonimowe (literały funkcji) mogą tworzyć zamknięcia, co oznacza, że mają dostęp do zmiennych z otaczającego kontekstu, nawet po jego zakończeniu. Takie zamknięcia alokują pamięć na heapie, jeśli jest to potrzebne do prawidłowego działania (detekcja odbywa się za pomocą analizy ucieczki).

Przykład:

func adder() func(int) int { sum := 0 return func(x int) int { sum += x return sum } } a := adder() printf("%d\n", a(5)) // 5 printf("%d\n", a(10)) // 15

Cechy szczególne:

  • Zamknięcie przechwytuje zmienne z zewnętrznego kontekstu przez referencję (a nie ich wartości w momencie tworzenia).
  • Jeśli zmienna jest zmieniana poza zamknięciem — zamknięcie zauważy nową wartość.
  • Jeśli zamknięcie jest zwracane z funkcji, przechwycone zmienne będą żyły do końca życia zamknięcia.
  • Jeśli zamknięcie nie jest używane, analiza ucieczki może umożliwić, aby zmienne nie trafiły na heap.

Pytanie z pułapką

Co wyświetli ten kod?

func main() { fs := []func(){} for i := 0; i < 3; i++ { fs = append(fs, func() { fmt.Println(i) }) } for _, f := range fs { f() } }

Wielu odpowiedziałoby, że wyświetli 0, 1, 2, jednak wynik będzie:

3
3
3

Wszystkie zamknięcia odnoszą się do tej samej zmiennej i; po zakończeniu pętli jej wartość wynosi 3.

Prawidłowo: przechwycić kopię zmiennej w ciele pętli:

for i := 0; i < 3; i++ { v := i // nowa zmienna fs = append(fs, func() { fmt.Println(v) }) }

Przykłady rzeczywistych błędów z powodu nieznajomości niuansów tematu


Historia

W projekcie dynamicznego routingu używano pętli do tworzenia wielu handlerów poprzez zamknięcia, z których każdy miał przechwycić swoją ścieżkę. W rezultacie wszystkie handlery drukowały ostatnią ścieżkę — nie stworzono oddzielnej zmiennej w każdym zamknięciu. Błąd został zauważony dopiero podczas integracji z API HTTP.


Historia

Podczas testowania równoległego dostępu przez gorutyny wewnątrz pętli zamknięcie przechwytywało referencję do indeksu, a nie kopię. Powodowało to "losowe" efekty: dane zapisywane były nie w swoim slocie tablicy, a w ostatnim.


Historia

W funkcji zbierania statystyk zamknięcie zmieniało wspólną zmienną z zewnętrznego kontekstu, chociaż autor oczekiwał niezależnego licznika dla każdego zadania. Problem zauważono po nieadekwatnie rekonstruowanej sumie, która zawsze była wspólna, a nie prywatna, pomimo lokalnej logiki.