Go ma ścisłe zasady inicjalizacji pakietów, zmiennych i funkcji podczas uruchamiania programu. Głównym mechanizmem jest wykonywanie funkcji init oraz inicjalizacja globalnych zmiennych. Poprawne zrozumienie tych procesów jest ważne dla zapobiegania błędom i niespodziewanym efektom.
Historia pytania:
W Go od samego początku wprowadzono ścisły podział faz uruchamiania: deklaracja, inicjalizacja i dalsze wykonanie kodu. W językach takich jak C/C++ często używa się konstruktorów zmiennych globalnych, w Go jednak kolejność inicjalizacji jest deterministyczna, ale ma swoje niuanse.
Problem:
Łatwo wpaść w pułapkę, kiedy inicjalizacja globalnych zmiennych lub wywołanie init prowadzi do wzajemnych zależności lub sytuacji cyklicznych między pakietami. Można to trudno śledzić, a programy mogą zachowywać się nie tak, jak oczekuje to programista, szczególnie przy ukrytych zależnościach lub hermetyzacji stanu na początku.
Rozwiązanie:
Pakiety w Go są inicjalizowane w kolejności określonej przez ich zależności: najpierw zależności, potem sam pakiet. Najpierw inicjalizowane są zmienne na poziomie pakietu (w kolejności pojawiania się w pliku źródłowym), a następnie wywoływana jest każda funkcja init(), jeśli taka istnieje. Można deklarować kilka init() w jednym pliku. Kolejność inicjalizacji między plikami jednego pakietu nie jest określona (i to może prowadzić do błędów).
Przykład kodu:
// a.go package main import "fmt" func init() { fmt.Println("init from a.go") } // b.go package main import "fmt" func init() { fmt.Println("init from b.go") }
Rezultat wykonania tych funkcji init nie jest przewidywalny między plikami w tym samym katalogu, ale zawsze przed funkcją main().
Kluczowe cechy:
Czy można polegać na kolejności wykonywania funkcji init w różnych plikach jednego pakietu?
Nie! Go nie gwarantuje kolejności między funkcjami init różnych plików w jednym pakiecie. Nadzieje na określoną kolejność mogą prowadzić do trudnych do zauważenia błędów i rozpadania się logiki biznesowej.
Czy globalne zmienne mogą być niezinicjalizowane w momencie wykonywania funkcji init?
Nie — wszystkie zmienne globalne pakietu są wykonywane ściśle w kolejności deklaracji przed wszystkimi funkcjami init tego pakietu. Wyjątki stanowią tylko krzyżowe inicjalizacje między pakietami (patrz poniżej).
Jak unikać cyklicznych zależności init między pakietami?
Go nie dopuszcza cyklicznych importów na poziomie pakietów (to błąd w czasie kompilacji), ale można wpaść w pułapkę pośredniej inicjalizacji: A zależy od B, B — od C, a C (poprzez zmienną globalną lub init) wywołuje kod z A. W takich przypadkach może wystąpić nieoczywisty porządek wywołań init/global. konstruktorów.
W zespole logiki inicjalizacji usług wykonane zostały w kilku funkcjach init w różnych plikach. Jedna init zależy od wyniku innej, co prowadzi do losowego zachowania między kompilacjami a uruchomieniem na różnych serwerach.
Zalety:
Wady:
Wszystkie stany i inicjalizacja wykonane są jawnie w main(). Funkcje init są używane wyłącznie do śledzenia uruchamiania i drobnych sprawdzeń.
Zalety:
Wady: