Having used defer in golang, I'm firmly in the camp that scope-based cleanup like defer is bad and value-based cleanup like destructors is better.
Say you have this (pseudocode):
function foo() {
let bar = make_bar();
defer cleanup_bar(bar);
let baz = make_baz();
defer cleanup_baz(baz);
let quux = make_quux();
defer cleanup_quux(quux);
do_something_with(bar, baz, quux);
}
Now imagine you have multiple functions like this that all create bar, baz and quux's. Say these are unit tests and the bar-baz-quux are mocks or whatever.
So you decide to DRY by moving the creation to a common function.
function create_mocks() -> (Bar, Baz, Quux) {
let bar = make_bar();
defer cleanup_bar(bar);
let baz = make_baz();
defer cleanup_baz(baz);
let quux = make_quux();
defer cleanup_quux(quux);
(bar, baz, quux)
}
... except this doesn't work any more, because the cleanup happens within create_mocks and create_mocks ends up returning cleaned-up values.
You could fix this by switching to a callback approach:
function run_with_mocks(f: function(Bar, Baz, Quux)) {
let bar = make_bar();
defer cleanup_bar(bar);
let baz = make_baz();
defer cleanup_baz(baz);
let quux = make_quux();
defer cleanup_quux(quux);
f(bar, baz, quux)
}
... but now you have to make the caller use a callback that necessarily returns void and not any other type. (This is mostly a golang problem, since languages with generics can be generic on the return type of the callback.) It also means you can't easily do things like set variables in an outer scope easily, depending on how the language deals with closure captures.
Worse, if not all tests use the same mocks, you actually end up with multiple of these functions:
function with_bar(f: function(Bar)) {
let bar = make_bar();
defer cleanup_bar(bar);
f(bar)
}
...
function foo() {
with_bar(bar => {
with_baz(baz => {
with_quux(quux => {
do_something_with(bar, baz, quux);
});
});
});
}
All these problems are avoided by having destructors.