More Gotchas of Defer in Go, Part II(blog.learngoprogramming.com) |
More Gotchas of Defer in Go, Part II(blog.learngoprogramming.com) |
It covers about half of these gotchas by laying out how the defer statement works in plain English. The other half (like how closures work inside loops in Go) are covered elsewhere in the language tour.
I do really like the visualizations! Makes it very clear how these mechanics work.
Btw, the first part was more advanced (but again for beginners and intermediate gophers) -> https://blog.learngoprogramming.com/gotchas-of-defer-in-go-1...
I fell to these when I was learning Go for the first time and then I've seen many people also did so. There is no rule like these are gotchas for everyone.
C++/CLI is a language that targets a garbage-collected runtime, yet has full-fledged RAII semantics (they're mapped to CLR Dispose pattern).
Swift and D also have garbage collectors and support RAII.
And since I already see it coming, reference counting is a garbage collection implementation algorithm in computer science literature.
for i := 0; i < 10; i++ {
i := i // Creates a *new* `i`
defer func() { … something with i … }
go func() { … something with i … }
}Lua 5.0 made just one loop variable per loop, like Go apparently does. When I first ran into this behavior, I thought that, although it wasn't what I expected, it made at least as much sense as what I was expecting. Since then, every single time I've been in a situation where the two ways of doing it gave different results, I've wanted the "separate variable for each iteration" behavior, so I was glad when Lua's creators changed loop variables to work like this in Lua 5.1.
(Before this change I would just do the Lua equivalent of your "Creates a new `i`" line.)
Shadowing bites you sooner or later if it becomes a habit and gains you almost nothing in real terms. I keep running into subtle nasties in coworkers' commits from quick'n'dirty-that-stayed "convenience" shadowings. Need I say `err`.. of course linters help, but aren't a given in a "bring-your-own chosen dev-env" team. =)
Very often yes I do.
Go encourages synchronous APIs, making it up to the caller to add concurrency. This is great, in my opinion. E.g. this is a common pattern:
var wg sync.WaitGroup
ch := make(chan int, len(items))
for _, item := range items {
item := item
wg.Add(1)
go func() {
defer wg.Done()
ch <- doSomething(item)
}()
}
wg.Wait()
close(ch)
for res := range ch {
…
}
Similar patterns with defer, although yes I'd say less often in a loop. Though in order to be "exception safe" (panic safe) I often do: foo, err := openFileOrSomething()
defer foo.Close()
[… do something …]
err := foo.Close()
So that even if "do something" throws exception… err… panics… the file gets closed. And double-closing is safe. That's not a closure though, in this example. So maybe not so good.I'm curious what good use-case there is for this, that the language bothered to support and allow this to even compile.
Duplicate variable name declaration + referencing in the same scope is unintuitive at best, and just seems wrong.
The reason why this idiom feels "weird" is that the loop construct of the language really ought to automatically make a fresh instance of the loop counter for each trip through the loop.
for i := 0; i < 3; i++ {
defer func(i int) {
fmt.Println(i)
}(i)
}
This shadows `i` pretty much the same amount as what I wrote. If `i` gets a different name inside the lambda, then it'd also be worse because then you could accidentally use `i` still. func f() {
x := ...
if x.something() {
x.doSomethingEarlier()
defer x.cleanup()
}
// use x however you like
}
where scope-based forces you to do stuff like func f() {
x := ...
if x.something() {
x.doSomethingEarlier()
defer x.cleanup()
// use x however you like
} else {
// use x however you like
}
}
In a scope-based defer, you'd have to keep all related code in the scope, nesting it another layer deeper / possibly duplicating it.On the flip-side is of course that this doesn't work like most would probably want in function-scoped:
for i := 0; i < 4; i++ {
x := get(i)
defer x.cleanup()
x.whatever()
}
and you're forced to for i := 0; i < 4; i++ {
x := get(i)
func() {
defer x.cleanup()
x.whatever()
}()
}
I've seen both of these patterns pretty frequently, in Go and in other languages. Go could, of course, have both a func_defer and a scope_defer, but that doesn't seem like it'd fit with the fairly strong focus on keeping the language feeling small and simple. So they had to pick one, and it can't handle both cases.So what I really wish was that languages used dot notation to go up scope eg ..name is that name two blocks out. This was one of the good parts of VB syntax which I miss in other languages. Think how much nicer it is than python's nonlocal and global, for example!
I would love to be able to explicitly affect other scopes as you mention, for example to define two classes at the same time.
But well, guess it comes down to subjective stylistic preferences here =)
> So that even if "do something" throws exception… err… panics… the file gets closed.
Your defer as placed in your above example is already scheduled to run always, even on a later panic. (After all, how else could one `recover` from a `panic` if it wasn't for `defer`?) I don't see the point of the double-closing at all here..
> Your defer as placed in your above example is already scheduled to run always
Yes, brainfart. Sorry. This is from a completely separate recommendation to defer a close (dropping error return), but also manually close so that closing errors can be surfaced. Matters for e.g. writing files (not so much reading), especially when you don't flush manually.
I mean, you can convert them into each other. Scoped can do something like this (go+python blended code 'cuz lazy):
func f(){
deferred := []
defer func() { for d in deferred.reverse(): d() }() // plus error handling
if x.something() {
deferred.push(func(){ cleanup() });
}
// same as func scope
}
but that's a bit more ridiculous / error-prone (though a helper func is obviously possible) than the equivalent IIFE for func -> scope. More explicit, I suppose, but bleh.Explicit-all-the-things isn't an unambiguous Good Thing™. If it were, we wouldn't even be discussing this - it's an abstraction, which is less explicit than e.g. building defer out of a list and using GOTO.
That's clearly false. You could set up a list to hold objects to be disposed (or, more generally, closures to execute) and defer a simple procedure that disposes of all objects in the list. This is in fact what the implementation of defer must do internally.
func someSQLStuff() {
tx, err := createTx()
defer func() {
if err != nil {
tx.Rollback()
log(err)
} else {
tx.Commit()
}
}()
rows, err = tx.QueryContext( ... )
// more SQL
}
Basically, function-scoped cleanup. Like closing opened files. func f(i interface{}) {
if closable, ok := i.(closable); ok {
defer closable.close()
}
// do stuff with i, maybe other casts, etc
}
There aren't many nice options for "if I can call X, defer a call to X" aside from shoving it into an `if`, where it'd be captured by that scope. I mean, you could do something like deferrable := func(){}
if closable {
deferrable = closable.close
}
defer deferrable()
but imagine doing that every time. It'd work, sure, but it'd also be more annoying. func closeIfNecessary(object interface{}) {
closable, needsClosing := object.(closable)
if needsClosing {
closable.close()
}
}
And then just do: func f(i interface{}) {
defer closeIfNecessary(i)
...
}
Doing it this way also saves you boilerplate by factoring the downcast out into a separate function.If you can't call it unless [some other conditions], it goes back to the same kind of problem though. "closable" may not be a good choice on my part, as they're often called unconditionally.