Description
I previously submitted the issue that the inclusion of a package can sometimes quintuple your binary's size because of the use of init functions. This issue was not original as others have reported the same:
- cmd/link: linker should be able to remove init functions with no side-effects #19533
- proposal: spec: lazy init imports to possibly import without side effects #38450
- layers.init cost too much time google/gopacket#101
At the current state of Golang, these issues are only blamed on the init functions in questions, citing that these init functions just need to be better written. I argue that init functions as a whole need to be removed for the reasons below, note that I do not blame "bad code" for their removal. I'd like to prove that init functions are objectively a bad feature.
Note: when I say "init function(s)", I mean both func init()
s and the initilizer variables (ie var myval = myValInit()
in global scope)
init functions make programmers avoid imports, hindering comprehensiveness
If a package has an init function that is taking a long time to run, maybe don't use that package?
That's a problem for programmers wanting to use some type
s found in a package but don't actually want to execute any of the functions. Importing a package to use just its type
s makes the programmer's executable/plugin have additional (and useless) execution thanks to init
. Sometimes forcing the programmer re-write the package's type
s, but doing so makes their executable/plugin incompatible/non-interchangeable with the original "upstream" package. Furthermore, that upstream package probably included important documentation and tests that are left behind by the programmer because all they require are the type
s, possibly hindering further work on the programmer's executable/plugin as well as have the programmer forced to manage even more code.
init functions have no way to allow the programmer to handle errors
Go is extremely focused on making sure the programmer focuses on handling errors rather than ignoring them. This was declared multiple times in both Go at Google as well as the FAQ.
However, init functions force everyone to ignore all possible errors. The author of an init function has no way to provide errors to the importer, so if an error would happen in an init function, more unexpected disasters will follow. A workaround for this would have the programmer check for the packages initError
(for example) to make sure the init function didn't run into an error... but at that point, you might as well have the programmer execute the init function explicitly because it's no longer automatic.
init functions are not explicit to the programmer's actions
You must be explicit with everything in golang. init functions are never called explicitly. One could argue that the act of importing a package is an explicit action to call the init function, but I disagree: you have to go through the entire package to find the init function as well as all init declarations, a laborious task that makes importing a package more of an investigation rather than an explicit execution.
I'd also like to point out golang's policy on implicit int conversions...
The convenience of automatic conversion between numeric types in C is outweighed by the confusion it causes. When is an expression unsigned? How big is the value? Does it overflow? Is the result portable, independent of the machine on which it executes?
For reasons of portability, we decided to make things clear and straightforward at the cost of some explicit conversions in the code.
... notice the length golang goes through to make things straightforward and explicit by disallowing the smallest things such as implicit int conversion. Yet importing a package can implicity call an unspecified amount of init functions executing an unspecified amount of code that performs an unspecified amount of tasks for unspecified reasons... and it does this recursively (as a single import can have even more imports). That is a MASSIVE amount of execution that occurs just because of a simple import.
Going back to Go at Google, Rob Pike himself was complaining about circular imports by saying the following:
More important, when allowed, in our experience such imports end up entangling huge swaths of the source tree into large subpieces that are difficult to manage independently, bloating binaries and complicating initialization, testing, refactoring, releasing, and other tasks of software development.
Notice Rob Pike's despise of bloating binaries and complicated initialization when talking about things to avoid when making golang's import system. These are the two very issues we're running into thanks to init functions.
In conclusion.
There's a ton of packages that depend on init functions right now, but users of those packages are surprised with huge bloats to their binaries for no obvious reasons as init functions are overly-implicit. Plus, in most cases we've experienced, the outcome of init functions rarely affects the use of the package. This is wrong, Golang is very much about explicitness over convenience.