Go and the pragmatic problems of having a Python-like with
statement
In a comment on my entry on finalizers in Go, Aneurin Price asked:
So there's no deterministic way to execute some code when an object goes out of scope? Does Go at least have something like Python's "with" statement? [...]
For those who haven't seen it, the Python with
statement is used
like this:
with open("output.txt", "w") as fp: ... do things with fp ... # fp is automatically closed by the # the time we get here.
Python's with
gives you reliable and automatic cleanup of fp
or whatever resource you're working with inside the with
block.
Your code doesn't have to know anything or do anything; all of the
magic is encapsulated inside with
and things that speak its
protocol.
Naturally, Go has no equivalent; sure, we have the defer
statement but
it's not anywhere near the same thing. In my opinion this is
the right call for Go, because of two issues you would have
if you tried to have something like Python's with
in Go.
The obvious issue is that you would need some sort of protocol to handle
initialization and cleanup, which would be a first for Go. You need the
protocol because a big point of Python's with
is that it magically
handles everything for you without you having to remember to write any
extra code; it's part of the point that using with
is easier and
shorter than trying to roll your own version (which encourages people to
use it). If you're willing to write extra code, Go has everything today
in the form of defer()
.
But beyond that there is a broader philosophical issue that's exposed by Aneurin Price's first question. In a language like Go where your local data may escape into functions you call, what does it mean for something to go out of scope? One answer is that things only go out of scope when there's no remaining reference to them. Unfortunately I believe that this is more or less impossible to implement efficiently without either going to Rust's extremes of ownership tracking in the language or forcing a reference counting garbage collector (where you know immediately when something is no longer referenced). This leaves you with the finalizer problem, where you're not actually cleaning up the resource promptly.
The other answer is that 'going out of scope' simply means 'execution reaches the end of the relevant block'. As in Python, you always invoke the cleanup actions at this point regardless of whether your resource may have escaped into things you've called and thus may still be alive somewhere. This implicit, hidden cleanup is a potentially dangerous trap for your code; if you forget and pass the resource to something that retains a reference to it, you may get explosions (much) later when that now-dead resource is used. If you're in luck, this use is deterministic so you can find it in tests. If you're unlucky, this use only happens in, say, an error path.
Using defer()
instead of an implicit cleanup doesn't stop this
problem from happening, but it makes explicit what's going on. When
you write or see a defer(fp.Close())
, you're pointedly reminded
that at the end of the function, the resource will be dead. There
is no implicit magic, only explicit actions, and hopefully this
creates enough warning and awareness. Given Go's design goals, being explicit here as part of the language
design makes complete sense to me. You can still get it wrong,
but at least the wrongness is more visible.
(I don't think being explicit is necessarily better in general than
Python's implicit magic. Go and Python are different languages with
different goals; what's appropriate for one is not necessarily
appropriate for the other. Python has both language features and
cultural features that make with
a good thing for it.)
|
|