Golang: How to sensibly use init() function?
Applications have initialization requirements. And init() function is Golang’s solution to the initialization problem. As part of this blog post, I will try to explain how to and how not to use the function.
A Brief introduction
But before I get started, let me quickly introduce the functionality. If you are already aware of what init() does, skip to the next section.
//Program needs squares of the first ten integers.
//It is sensible to initialize it once and use it multiple times in the programvar squares []intfunc init() {
for i := 0; i < 10; i++ {
squares = append(squares, i*i)
}
}func main() {
fmt.Printf("Square Values: %+v", squares)
}
//Prints: Square Values: [0 1 4 9 16 25 36 49 64 81]
The above example shows an example of the init() function. This function is executed before main(). There is more to init(), following are the key features:
- There can be multiple init() functions in a package or even in a single file
- The order of execution of multiple init()’s is decided by the order in which the go files are passed to go build command — It is recommended that the go files are passed to go build command in lexical order.
- Within a single file, if multiple init() functions are defined, they will be invoked in the order they are defined.
- If test file has an init() function, then that init() will be executed before the test is executed. In other words, first, all not-test files init() functions are executed, then all test files init() functions are executed (hopefully in lexical order).
- Application programmer cannot invoke init() functions. That means they cannot test the function either.
- Even if you initialize the package multiple times, init() function will be invoked only once.
Now, I will discuss a few scenarios where it is very tempting to use init() function, but let’s understand the implications.
Initializing external resources
How about initializing external resources — such as database connection pools, external configuration files, etc.?
Well, it seems sensible to initialize external resources in init() but it has two problems:
- What will you do if the external resource fails? If its okay to panic and end the application, then maybe init() is still fine but if it is not okay to panic and you want to handle the application error more tactfully — init() is not right for you?
- init() function will be invoked before running test cases — and you may not want that — your test cases may not require database resource (maybe because you are mocking database access layer).
It may be more sensible to do the initialization in the main() function or elsewhere — where error handling is still possible. Also, it will not be required to write some funky code to skip the init() function for unit testing.
Something that requires re-initialization
I already mentioned it — you cannot invoke init() functions from code.
//Program starts with an array of balance values as 100.
//The program is supposed to print current business values every two seconds and re-initialize the list back to original values of 100.00var balanceValues []intfunc init() {
for i := 0; i < 10; i++ {
balanceValues = append(balanceValues, 100.00)
}
}func main() {
timer1 := time.NewTimer(2 * time.Second)
go func() {
<-timer1.C
fmt.Println("Current Balance Values", balanceValues)
init() // Will fail - cannot invoke init() from code.
}()
// Business logic to change balanceValues
balanceValues[1] = 100}* For this example, please ignore the data consistency issues in the above example (timer go-routine and business logic main() function are accessing the same array). This discussion is focused on re-initialization.
Something that requires unit-testing
If the initialization logic is not trivial and you want to unit test it — you are out of luck with init(). Simply, because you cannot invoke init().
You can come up with a “hack” like below:
func init() {
realInitialization()
}// And then unit test realInitialization().
Note that this “hack” solution will also work for the Re-Initialization problem discussed above. But, it is a hack.
Conclusion
I am not a huge fan of init() functionality. I have used it for trivial initializations (of global variables and you shouldn’t have global variables)— as explained in the first example above. But then you don’t have consistency with your initialization code — and that kind of beats the purpose of having this feature.
This is what I normally end up doing. I create a function that serves the purpose of initializing a package. hen I expect users of the package to explicitly invoke the function — this initialization mostly happens in main.
There are advantages to explicit invocation:
- Explicit invocation makes initialization step very clear — and removes any magic (auto initialization) what-so-ever.
- I get complete control over unit tests
- I get complete control over exception handling