Go has strict rules for package, variable, and function initialization when starting a program. The main mechanism is the execution of init functions and initialization of global variables. Understanding these processes correctly is important to prevent errors and unexpected effects.
Background:
Go has introduced a strict separation of startup phases from the very beginning: declaration, initialization, and further code execution. In languages like C/C++, global variable constructors are often used, while in Go the order of initialization is deterministic, but there are nuances.
Problem:
It is easy to fall into the trap where the initialization of global variables or the call to init leads to interdependent or cyclic situations between packages. This is difficult to track, and programs may behave unexpectedly, especially with hidden dependencies or encapsulated state at startup.
Solution:
Packages in Go are initialized in the order determined by their dependencies: first dependencies, then the package itself. Package-level variables are initialized first (in the order they appear in the source file), and then any init() function is called if it exists. Multiple init() functions can be declared in one file. The order of initialization between files of the same package is not defined (and this can lead to errors).
Example code:
// 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") }
The execution result of these init functions is unpredictable between files within the same directory, but always occurs before the main() function.
Key features:
Can you rely on the order of execution of init functions in different files of the same package?
No! Go does not guarantee the order between init functions of different files in the same package. Hopes for a specific order can lead to elusive bugs and fragmentation of business logic.
Can global variables be uninitialized by the time the init function executes?
No — all global variables of the package are executed strictly in declaration order before any init functions of that package. Exceptions are only for cross-initializations between packages (see below).
How to avoid cyclic dependencies between init functions across packages?
Go does not allow cyclic imports at the package level (this is a compile-time error), but one can fall into the trap of indirect initialization: A depends on B, B depends on C, and C (through a global variable or init) calls code from A. In such cases, a non-obvious calling order of init/global constructors can arise.
In a team, the initialization logic of services is executed in several init functions across different files. One init function depends on the result of another, leading to unpredictable behavior between builds and when running on different servers.
Pros:
Cons:
All state and initialization are performed with explicit calls in main(). init functions are used solely for tracing startup and small checks.
Pros:
Cons: