ProgrammatieSenior Go ontwikkelaar

Wat zijn de kenmerken van het werken met globale variabelen in Go, hoe te voorkomen dat ze verkeerd worden geïnitialiseerd en concurrentie tijdens de opstartvolgorde (init order)? Wat zijn de typische valkuilen?

Slaag voor sollicitatiegesprekken met de Hintsage AI-assistent

Antwoord.

In Go worden globale variabelen geïnitieerd volgens de volgorde van bestandverklaringen en de volgorde van hun init-functies. Dit bepaalt de correctheid van de toegang tot de globale toestand: als een variabele in het ene pakket afhankelijk is van de waarde van een variabele in een ander pakket, maar dat pakket is nog niet geïnitialiseerd — zal er een fout of onvoorspelbaar gedrag optreden.

Er kan geen directe "startvolgorde" worden ingesteld tussen init-functies van verschillende pakketten. De compiler bepaalt deze volgorde op basis van de importgrafiek.

  • Alle globale variabelen worden geïnitialiseerd voordat de functie main.main() wordt aangeroepen.
  • Het wordt niet aanbevolen om de toestand te initialiseren met buitensporige afhankelijkheden tijdens package init!
  • Voor thread-veiligheid van globale variabelen worden sync.Once of mutexen gebruikt bij complexe initialisatie.

Voorbeeld van veilige uitgestelde initialisatie (lazy init):

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

Strikvraag.

Vraag: Kunnen init-functies van verschillende pakketten tegelijkertijd (parallel) worden uitgevoerd bij het starten van een Go-programma?

Correct antwoord: Nee, init-functies en globale variabelen worden strikt sequentieel uitgevoerd in de volgorde die door de compiler is bepaald bij het analyseren van de afhankelijkheden tussen pakketten. Parallelle uitvoering van init-functies vindt niet plaats.


Voorbeelden van echte fouten door gebrek aan kennis over de nuances van het onderwerp.


Verhaal

In een groot project laadden verschillende modules globale configuraties vanaf de schijf via init(). Eén module was afhankelijk van een andere, maar door de herstructurering van de go.mod-grafiek was de initialisatievolgorde veranderd — en plotseling bleken de waarden van de configuraties leeg te zijn! De fout kwam willekeurig voor, afhankelijk van de volgorde van de compilatie, en werd pas gevonden na het analyseren van de afhankelijkheden en het omzetten van de initialisatie naar een expliciete functie.


Verhaal

In een REST-service werd een globale map zonder mutex gebruikt voor het cachen van referentiedatabases. De initialisatie begon vanuit verschillende goroutines die in init werden gestart. Het resultaat — data race, periodieke paniek, ongeldige gegevens binnen de map. Na vervanging door sync.Once en expliciete aanroep van de initialisatie verdween het probleem.


Verhaal

Een globale logger werd gecreëerd in de init-functie van een van de hulppakketten, maar een ander pakket benaderde deze logger ook bij de opstart, voordat deze was geïnitialiseerd. Het resultaat was dat een deel van de opstartlogs verloren ging, terwijl de logger nog niet bestond. De oplossing — afhankelijkheidsinjectie en expliciete initialisatie van de logger vóór het starten van alle services.