In fact this was so surprising to me is that I only found out about it when I wrote code that processed files in a loop, and it started crashing once the list of files got too big, because defer didnt close the handles until the function returned.
When I asked some other Go programmers, they told me to wrap the loop body in an anonymus func and invoke that.
Other than that (and some other niggles), I find Go a pleasant, compact language, with an efficient syntax, that kind of doesn't really encourage people trying to be cute. I started my Go journey rewriting a fairly substantial C# project, and was surprised to learn that despite it having like 10% of the features of C#, the code ended up being smaller. It also encourages performant defaults, like not forcing GC allocation at every turn, very good and built-in support for codegen for stuff like serialization, and no insistence to 'eat the world' like C# does with stuff like ORMs that showcase you can write C# instead of SQL for RDBMS and doing GRPC by annotating C# objects. In Go, you do SQL by writing SQL, and you od GRPC by writing protobuf specs.
Right now it's function scope; if you need it lexical scope, you can wrap it in a function.
Suppose it were lexical scope and you needed it function scope. Then what do you do?
You can just introduce a new scope wherever you want with {} in sane languages, to control the required behavior as you wish.
Defer a bulk thing at the function scope level, and append files to an array after opening them.
It can be also a source of bugs where you hang onto something for longer than intended - considering there's no indication of something that might block in Go, you can acquire a mutex, defer the release, and be surprised when some function call ends up blocking, and your whole program hangs for a second.
Would be nice to have both options though. Why not a “defer” package?
if (some condition) { defer x() }
When it's lexically scoped, you'd need to add some variable. Not that that happens a lot, but a lexically scoped defer isnt needed often either.2. mechanic is tied to call stack / stack unwinding
3. it feels natural when you're coming from C with `goto fail`
(yes it annoys me when I want to defer in a loop & now that loop body needs to be a function)
But I do definitely agree that the dynamic nature of defer and it not being block-scoped is probably not the best
I can't recall ever needing that (but that might just be because I'm used to lexical scoping for defer-type constructs / RAII).
f.Close() // without defer
The reason I might want function scope defer is because there might be a lot of different exit points from that function.With lexical scope, there’s only three ways to safely jump the scope:
1. reaching the end of the procedure, in which case you don’t need a defer)
2. A ‘return’, in which case you’re also exiting the function scope
3. a ‘break’ or ‘continue’, which admittedly could see the benefit of a lexical scope defer but they’re also generally trivial to break into their own functions; and arguably should be if your code is getting complex enough that you’ve got enough branches to want a defer.
If Go had other control flows like try/catch, and so on and so forth, then there would be a stronger case for lexical defer. But it’s not really a problem for anyone aside those who are also looking for other features that Go also doesn’t support.
try (SomeResource foo = SomeResource.open()) { method(foo); }
or
public void method() { try(...) { // all business logic wrapped in try-with-resources } }
To me it seems like lexical scoping can accomplish nearly everything functional scoping can, just without any surprising behavior.
Since they didn't want to have a 'proper' RAII unwinding mechanism, this is the crappy compromise they came up with.
But that’s a moot point because I appreciate it’s just an example. And, more importantly, Go doesn’t support the kind of control flows you’re describing anyway (as I said in my previous post).
A lot of the comments here about ‘defer’ make sense in other languages that have different idioms and features to Go. But they don’t apply directly to Go because you’d have to make other changes to the language first (eg implementing try blocks).
a := Start()
if thingEnabled {
thing := connectToThing()
defer thing.Close()
a.SetThing(thing)
}
a.Run(ctx)Another example I found in my code is a conditional lock. The code runs through a list of objects it might have to update (note: it is only called in one thread). As an optimization, it doesn't acquire a lock on the list until it finds an object that has to be changed. That allows other threads to use/lock that list in the meantime instead of waiting until the list scan has finished.
I now realize I could have used an RWLock...
Using `defer...recover` is computationally expensive within hot paths. And since Go encourages errors to be surfaced via the `error` type, when writing idomatic Go you don't actually need to raise exceptions all that often.
So panics are reserved for instances where your code reaches a point that it cannot reasonably continue.
This means you want to catch panics at boundary points in your code.
Given that global state is an anti-pattern in any language, you'd want to wrap your mutex, file, whatever operations in a `struct` or its own package and instantiate it there. So you can have a destructor on that which is still caught by panic and not overly reliant on `defer` to achieve it.
This actually leads to my biggest pet peeve in Go. It's not `x, err := someFunction()` and nor is it `defer/panic`, these are all just ugly syntax that doesn't actually slow you down. My biggest complaint is the lack of creator and destructor methods for structs.
the `NewClass`-style way of initialising types is an ugly workaround and it constantly requires checking if libraries require manual initilisation before use. Not a massive time sink but it's not something the IDE can easily hint to you so you do get pulled from your flow to then either Google that library or check what `New...` functions are defined in the IDEs syntax completion. Either way, it's a distraction.
The lack of a destructor, though, does really show up all the other weaknesses in Go. It then makes `defer` so much more important than it otherwise should be. It means the language then needs runtime hacks for you to add your own dereference hooks[0][1] (this is a problem I run into often with CGO where I do need to deallocate, for example, texture data from the GPU). And it means you need to check each struct to see if it includes a `Close()` method.
I've heard the argument against destructors is that they don't catch errors. But the counterargument to that is the `defer x.Close()` idiom, where errors are ignored anyway.
I think that change, and tuples too so `err` doesn't always have to be its own variable, would transform Go significantly into something that feels just as ergonomic as it is simple to learn, and without harming the readability of Go code.
[0] https://medium.com/@ksandeeptech07/improved-finalizers-in-go...
[1] eg https://github.com/lmorg/ttyphoon/blob/main/window/backend/r...