Menu

defer w Go (Golang): kolejność, ewaluacja argumentów i pułapki

defer planuje wywołanie, które wykona się, gdy otaczająca funkcja zwróci wynik. Poznaj kolejność LIFO, moment ewaluacji argumentów, zamykanie plików i zwalnianie mutexów, defer w pętlach i zmianę nazwanych wyników.

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

Co robi defer

defer dokłada wywołanie funkcji do listy. Gdy otaczająca funkcja zwraca wynik, lista wykonuje się w odwrotnej kolejności.

Wynik:

start
end
deferred 3
deferred 2
deferred 1

Kolejność to ostatnie weszło, pierwsze wychodzi, jak na stosie. Pasuje to do zagnieżdżania zasobów: jeśli otwierasz A, a potem B, zwykle chcesz zamknąć B przed A.

Sprzątanie obok pozyskania zasobu

Główne zastosowanie defer to zapisanie sprzątania w linii tuż po pozyskaniu zasobu, żeby żadna ścieżka powrotu nie mogła o nim zapomnieć.

Zwróć uwagę na kolejność: najpierw sprawdź błąd, potem defer. Jeśli os.Open się nie powiodło, f jest nil, a odroczenie f.Close() przed sprawdzeniem wywołałoby Close na nilowym *os.File (co zwraca błąd, którego nigdy nie zobaczysz, i wprowadza czytelników w błąd).

Ten sam schemat działa z blokadami:

mu.Lock()
defer mu.Unlock()

Jeśli kod pomiędzy wywoła panic, mutex i tak zostanie zwolniony.

Argumenty są obliczane od razu

Odroczona funkcja i jej argumenty są obliczane, gdy wykonuje się instrukcja defer. Czeka tylko samo wywołanie.

Wynik:

x is now 2
deferred closure reads: 2
deferred with argument: 1

fmt.Println("...", x) zapamiętało wartość 1 w linii z defer. Domknięcie nie ma argumentów; czyta x dopiero wtedy, gdy w końcu się wykona. Wybierz formę, która odpowiada temu, co chcesz zapisać.

Dotyczy to też odbiorców metod. defer t.Stop() od razu oblicza t, więc późniejsze przypisanie nowej wartości do t nie zmienia tego, która wartość zostanie zatrzymana.

Popularna sztuczka do mierzenia czasu celowo wykorzystuje tę zasadę:

func handle() {
	defer trace("handle")() // trace runs now, the returned func runs at exit
	// ...
}

trace("handle") jest wywoływane od razu (może wypisać "enter" i zapisać czas startu), a odroczona zostaje funkcja, którą zwraca.

Defer w pętli

Odroczone wywołania wykonują się, gdy funkcja zwraca wynik, a nie na końcu każdej iteracji pętli. W pętli po wielu plikach każdy plik pozostaje otwarty aż do końca funkcji.

for _, path := range paths {
	f, err := os.Open(path)
	if err != nil {
		return err
	}
	defer f.Close() // all files stay open until the function returns
	process(f)
}

Przy tysiącach ścieżek skończą się deskryptory plików. Przenieś ciało pętli do osobnej funkcji, żeby każdy defer wykonywał się w każdej iteracji:

Każde wywołanie processFile zamyka swój plik, zanim otworzy się następny. Literał funkcji wywołany w miejscu (func() { ... }()) działa tak samo, gdy nazwana funkcja pomocnicza wydaje się przesadą.

Zmiana zwracanych wartości

Odroczone domknięcie wykonuje się po tym, jak instrukcja return przypisała wyniki, i może zmienić nazwane wyniki, zanim zobaczy je wywołujący.

To wypisuje 10 oraz save failed: disk full. Przy nienazwanym wyniku odroczona funkcja nadal może się wykonać, ale nie ma jak zmienić tego, co zostaje zwrócone.

Przechwytywanie błędu z Close

defer f.Close() wyrzuca błąd z Close. Dla plików, które tylko czytasz, to w porządku. Dla plików, do których zapisujesz, Close może zgłosić nieudany ostatni zapis bufora, więc błąd ma znaczenie. Nazwany wynik pozwala go zachować:

func writeReport(path string, data []byte) (err error) {
	f, err := os.Create(path)
	if err != nil {
		return err
	}
	defer func() {
		if cerr := f.Close(); cerr != nil && err == nil {
			err = cerr
		}
	}()
	_, err = f.Write(data)
	return err
}

errors.Join(err, f.Close()) to krótsza alternatywa, gdy chcesz zgłosić oba błędy.

Defer, panic i recover

Odroczone wywołania wykonują się, gdy panic zwija stos. To jedyne miejsce, w którym recover cokolwiek robi, i w ten sposób serwer chroni proces przed tym, żeby jedno złe żądanie go zabiło. Szczegóły znajdziesz na stronie panic i recover.

Odroczone wywołania nie wykonują się, gdy program kończy się przez os.Exit lub log.Fatal. Jeśli main odracza sprzątanie, a potem wywołuje os.Exit(1), sprzątanie zostaje pominięte.

Koszt

Od Go 1.14 większość instrukcji defer kompilator wbudowuje bezpośrednio w kod (open-coded) i kosztują one kilka nanosekund. Używanie defer przy każdym zwalnianiu mutexa i zamykaniu pliku to normalny styl. Wyjątkiem są defer w pętlach: tych nie da się wbudować i korzystają z wolniejszej ścieżki, co jest kolejnym powodem, żeby przenosić ciała pętli do funkcji.

Typowe błędy

  • Defer przed sprawdzeniem błędu. Najpierw sprawdź err z Open, potem defer Close.
  • Oczekiwanie sprzątania w każdej iteracji pętli. Defer wykonuje się przy wyjściu z funkcji.
  • Oczekiwanie, że odroczony argument zobaczy późniejsze zmiany. Argumenty są ustalane w linii z defer. Użyj domknięcia, żeby czytać przy wyjściu.
  • Poleganie na defer przy os.Exit. Nigdy się nie wykona.

Najczęściej zadawane pytania

Co robi defer w Go?

defer f() planuje wykonanie f() w chwili, gdy otaczająca funkcja zwraca wynik, niezależnie od tego, czy kończy się normalnie, przez wcześniejsze return, czy przez panic. Służy do umieszczenia sprzątania (zamknięcia pliku, zwolnienia mutexa) tuż obok kodu, który zajął zasób.

W jakiej kolejności wykonują się odroczone wywołania w Go?

Ostatnie weszło, pierwsze wychodzi (LIFO). Najpierw wykonuje się wywołanie odroczone najpóźniej. defer fmt.Println(1); defer fmt.Println(2) wypisuje 2, a potem 1.

Kiedy obliczane są argumenty odroczonej funkcji?

Od razu, gdy wykonuje się instrukcja defer, a nie gdy uruchamia się wywołanie. x := 1; defer fmt.Println(x); x = 2 wypisuje 1. Żeby odczytać wartość przy wyjściu, odrocz domknięcie: defer func() { fmt.Println(x) }().

Czy defer wykonuje się przy panic albo os.Exit?

Odroczone wywołania wykonują się, gdy panic zwija stos, i dlatego recover działa w ich wnętrzu. Nie wykonują się, gdy program wywołuje os.Exit (albo log.Fatal, które je wywołuje), ani w innych gorutynach, gdy main zwraca.

Ilustracja języków programowania w Coddy

Ucz się programowania z Coddy

ZACZNIJ