programowanieStarszy programista Go

Jakie są cechy pracy z globalnymi zmiennymi w Go, jak uniknąć ich nieprawidłowej inicjalizacji i konkurencji w czasie uruchamiania (kolejność init)? Na czym polegają typowe pułapki?

Zdaj rozmowy kwalifikacyjne z asystentem AI Hintsage

Odpowiedź.

W Go globalne zmienne są inicjalizowane w porządku deklaracji plików i sekwencji ich funkcji init. Od tego zależy poprawność dostępu do globalnego stanu: jeśli zmienna w jednym pakiecie zależy od wartości zmiennej innego pakietu, ale ten pakiet nie został jeszcze zainicjalizowany — wystąpi błąd lub nieoczekiwane zachowanie.

Nie można bezpośrednio ustalić "kolejności startowej" między funkcjami init różnych pakietów. Kompilator określa tę kolejność na podstawie grafu importu.

  • Wszystkie globalne zmienne są inicjalizowane przed wywołaniem funkcji main.main().
  • Nie zaleca się inicjalizacji stanu z nadmiernymi zależnościami podczas inicjalizacji pakietu!
  • Aby zapewnić bezpieczeństwo wątkowe globalnych zmiennych, używa się sync.Once lub mutexów podczas skomplikowanej inicjalizacji.

Przykład bezpiecznej, opóźnionej inicjalizacji (lazy init):

var cfg *Config var once sync.Once func GetConfig() *Config { once.Do(func() { cfg = LoadConfigFromDisk() }) return cfg }

Pytanie z haczykiem.

Pytanie: Czy funkcje init różnych pakietów mogą działać równocześnie (równolegle) podczas uruchamiania programu w Go?

Poprawna odpowiedź: Nie, funkcje init i globalne zmienne są wykonywane ściśle sekwencyjnie w kolejności określonej przez kompilator podczas analizy zależności między pakietami. Nie ma równoległego uruchamiania funkcji init.


Przykłady rzeczywistych błędów wynikających z niewiedzy o subtelnościach tematu.


Historia

W dużym projekcie kilka modułów ładowało globalne konfiguracje z dysku przez init(). Jeden moduł zależał od drugiego, ale z powodu przebudowy grafu go.mod, kolejność inicjalizacji się zmieniła — w rezultacie wartości konfiguracji okazały się puste! Błąd objawiał się losowo, w zależności od kolejności kompilacji, i został znaleziony dopiero po przeanalizowaniu zależności i przeniesieniu inicjalizacji do jawnej funkcji.


Historia

W serwisie REST do buforowania słowników używano globalnej mapy bez mutexa. Inicjalizacja rozpoczynała się z kilku goroutine, które startowały w init. W efekcie — data race, okresowe paniki, nieprawidłowe dane wewnątrz mapy. Po zamianie na sync.Once i jawnym wywołaniu inicjalizacji problem zniknął.


Historia

Globalny logger był tworzony w funkcji init jednego z pomocniczych pakietów, jednak inny pakiet odwoływał się do tego loggera również przy starcie, przed jego inicjalizacją. W efekcie część logów o starcie ginęła, dopóki logger jeszcze nie istniał. Rozwiązanie — wstrzykiwanie zależności i jawna inicjalizacja loggera przed uruchomieniem wszystkich serwisów.