Go is the language with the automatic memory management: garbage collector without a programmer participation will free up memory previously allocated for objects that are not in use anymore. But all automation in general is limited to memory and we still need to manually clean up all other resources accuired by the program.
The only thing that the GC can offer for resource cleanup automation is to call a finalizer registered via the runtime.SetFinalizer function. But this mechanism does not guarantee either the finalzers call order or even that the finalizer will be called at all. This behaviour is not specific for Go — other languages that supports a finalization acts in the same way and as a consequence it is not recomended to rely on finalization.
But manually resource cleanup without special tools can be tedious and error-prone. Particularly the GC as such was developed to solve those problems in context of the memory management. There are too many places where is possible to make a mistake in a code like this:
And even if no mistakes, a panic in any called function will negate all efforts (exceptions lead a similar problem in other languages). If Close functions return errors things are getting worse — often this errors are ignored at all.
Different languages provides different tools that simplify the resource cleanup. For example, C# and Java provides using statement and try-with-recources statement respectively. Go provides defer statement that is much more flexible IMO. The code above rewritten with it becomes easier both to write and maintain:
This code already has a guarantee of calling Close even if panic occurred. Although handling of Close errors is still bad.
It would seem that this is enough in almost any situation. But defer is applicable only within a single function. You can’t use it in a constructor for instance — all initialized resources will be finalized on return despite they are still needed by the program. Of course, it can be argued that it would be better to use Dependency Injection in such cases. However, even apart from the fact that some resources can still be encapsulated, dependencies still need to be initialized somewhere. It means that we need to combine initialization with main logic in a single function (unfortunately often it is main) or we can use some kind of Inversion of Control (e. g. callback). But in both cases initialization and main logic are more coupled than we’d like to be.
A good example of the described problem is the code generated by the Wire. Initializer (provider in terms of Wire) may return so-called cleanup function that would be used to finalize a provided resource. But the generated code will be more like the first gist in this article and will break if panic occurred.
However, the idea of returning the cleanup function proposed by the Wire looks promising itself IMO. The Close method (or a similar finalizer) has two drawbacks at once:
- you can simply forget to call the method (there are no control by the language);
- the method may be called by a resource-dependent function by mistake.
But when the finalizer is returned not as a part of the resource, but as a separate entity associated with it, the second drawback is excluded because a dependent function has no interface for a mistake call. The first drawback is also excluded by the fact that a declared but unused variable is a compile-time error in Go:
So three output parameters (resource, cleanup function and error) instead of usual two (resource and error) are look like a good solution. As a bonus, it allows us to introduce the concept of “resource ownership” which means a resource belongs to an entity that owns its finalizer and all others “borrow” or “rent” this resource without interacting with the interface for managing its lifecycle.
To make it possible to use defer statement (or rather the most similar mechanism) in initializers we can do something like this:
The Finalize function and the common part of initializer are good candidates to extract them to the library.
In addition to the ability to use the familiar new — error — defer pattern, the proposed approach after some refinement also provides a mechanism for convenient handling of finalization errors, which are often ignored even in the official documentation, since their handling is very confusing.
KDone as a consulusion
I’ve published the KDone library that provides tools described above. It’s a part of the Kata project which I’ll tell in the next time. The API provided by the library is mostly stable and will not change significantly. However, I still use zero major version.
A typical initializer (constructor) with use of the library looks like this:
Thank you for your attention!