-
Notifications
You must be signed in to change notification settings - Fork 18k
proposal: Go 2: spec: remove init functions #43731
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
While language changes do not have to be backward compatible, it's hard to see how we could adopt a language change that would break an enormous number of existing packages. |
Perhaps in the future, legacy imports could be given an optional "flag" that will allow init functions to be executed. That way, the explicitness is preserved without leaving unmaintained packages broken. |
It seems that the desire here is to let packages be lazily initialized only when they are needed. But the language already supports this. We don't need to disallow initializers to permit lazy initialization. Removing initializers removes a widely used convenient mechanism to safely ensure that package invariants are established. Go has gone to a great deal of trouble to get initializers right, in the sense that they work reliably and predictably, which is not true in some other languages. It seems very hard to justify removing this existing feature. If we did accept this idea, how do you suggest that existing code be rewritten systematically and safely to avoid initializers? |
While I agree that using init functions is often not needed, they are essential to GUI or game libraries in Go which must lock the OS thread for technical reasons. Hence init remains needed for this use at least. |
So as of now, importing a package has the following syntax:
And as I've mentioned in my previous comment, I suggested adding a flag that would enable init functions would preserve go's explicitness. However, after putting some thought into it, I propose the inverse of that property. This means the programmer can specify a flag on a particular import that will inform the compiler NOT to include the init functions (as well as function initializers). For example:
Here I use the fictitious token "idle" for its specific meaning of "avoiding work". Other vocabularies to consider: "passive", "limp", "still", "rigid", "ossified", "inert", "static". So long that the vocabulary's connotation has a natural tendency to deter its use by newer programmers (as this feature can indeed be confusing unless the programmer knows what he/she is doing). And the exact syntax is also subject to debate, mainly in regards to option 1 which allows multiple idle imports, or option 2 making it more deliberate per-import. Overall I consider this an elegant solution. I'd suggest we actually put this in Go 1 because it will solve the larger problems I originally mentioned without forsaking the function of packages that rely on init functions such as the ones @beoran pointed out. It simply wouldn't affect anything other than offering programmers better options for imports. However, for Go 2, we are still limited by error handling and falling short of being an explicit language, for that reason I'd recommend my former solution of either getting rid of them completely or making init functions explicit. Semantically there are still some questions to be answered... and admittedly I'm not prepared to have answered them because I cannot predict their impacts without some further feedback.
|
If a package can be imported without running its initializers, then there must be some mechanism to let that package work even though it was not initialized. If there is no such mechanism, then using |
Indeed, There have been dozens of times I had to import a package into mine because I only required a handful of types and/or functions from that package only to be bloated by init functions that have nothing/no effect on what I was using the package for. A normal |
I think init() functions are certainly overused, and should be replaced by explicit initialization wherever possible. However, we can do this without changing the language. The init function should be documented in such a way to dissuade it's use it except for special cases. As for existing packages, if they are under an open source license, you may fork them to get rid of the init(). An import idle is a bad idea though, because you are trying to override in one package what another package will do when imported. This is a 'spooky action at distance'. Better modify the imported package to do what you want, if possible. |
I can't agree. Doing too much may not be ideal, but it is not unsafe. |
This is certainly a great start. I'd consider this a proper solution for the time being. |
Does this come up often? Typically if I want types from a package, I want functions and maybe variables from it too. And those functions/variables may depend on the package initializers for correct behavior. If we had evidence that this use case really did come up often, I think we could add a "types-only" import where you only get access to the types and consts declared by a package, and it doesn't force initializers to run. However, this would have to be fairly restricted. E.g., we'd probably need to disallow storing types that are imported this way into an interface, unless the type's method set is empty. I suspect it comes up rarely enough that it would be simpler for the package authors to just take responsibility for factoring an appropriate initializer-free subset. Separately, I'll note there's work-in-progress for Go 1.17 to automatically remove side-effect-free initializers from programs when they're not needed. E.g., a global map that's initialized at program startup, but then none of the code that uses it is included into the final executable. |
Based on the discussion above, and the lack of strong support in the emoji voting, this is a likely decline. Leaving open for four weeks for final comments. |
No further comments. Closing. |
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:
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 (ievar myval = myValInit()
in global scope)init functions make programmers avoid imports, hindering comprehensiveness
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 itstype
s makes the programmer's executable/plugin have additional (and useless) execution thanks toinit
. Sometimes forcing the programmer re-write the package'stype
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 thetype
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...
... 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:
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.
The text was updated successfully, but these errors were encountered: