6 ms·
> Go has it's own special defer which only fires on function end, otherwise defer has consistent "execute at scope end" semantics. Does anyone know why Go’s `d
by pansa2 6y ago
> Go has it's own special defer which only fires on function end, otherwise defer has consistent "execute at scope end" semantics.
Does anyone know why Go’s `defer` semantics is so unintuitive? Why is `defer` function-based rather than block-based, in a language where variables are block-scoped?
- lerno 6y agoIf I may venture a guess, it is because this way there is no limit in regard to jumps within or out of a function. Since Go retains `goto`, function based is easier to implement. In addition, non-local jumps, such ones constructed by setjmp, are fairly easily easy to resolve (just say that the jump will release all queued defers before the jump). But I've also heard the opinion that some find the function based `defer` easier to reason about, so maybe that weighs in as well.
- iainmerrick 6y agoI suspect that it’s a side-effect of Go’s origin in the Plan 9 compiler chain. This is really just speculation, but I’ll try to justify it. First, early versions of Go strictly required a “return” as the final statement in a function, rather than just checking that each branch terminated successfully (i.e. you couldn’t return from both branches of an if/else, you also needed a redundant return statement afterwards). So I think initially the compiler didn’t perform the kind of escape analysis that would make block-scoped “defer” easy to implement. Second, block-scoped defer is really equivalent to RAII via constructors and destructors in C++. Go was specifically designed as an anti-C++, an attempt to return to the roots of C and improve on it, so the designers would have been very wary of copying C++ features like RAII. I think it was a mis-step, and they should have gone with block-scoped. Other languages have copied Go’s syntax, but they’ve uniformly opted for C++’s semantics, which in this instance are much easier to reason about (definitely not always the case with C++!)
- knorker 6y ago> Go was specifically designed as an anti-C++ Ha! And still they managed to reimplement RAII and exceptions, only very very poorly.
- eknkc 6y agoOne benefit of function based defer is that you can use anonymous functions to create hacky block-based defers. If the language only has block based defers then that's it. You have that and only that. What if I create a resource in an if block and want to defer closing it when the function ends?
- dthul 6y agoDepending on how the defer implementation looks like the same could be achieved with block-level defer by having the defer's "owner" (e.g. some object with RAII semantics) in the outer scope.
- bbatha 6y agoYou can get function based defers pretty easily with block ones too func Deferred() { defers := []func(){} defer func() { for _, def := range defers { def() } } for { cleanup, done := work() defers = append(defers, cleanup) if done { break } } } Edit: this is how defers were implemented in the compiler until a few versions ago and will fall back to this if you defer in a loop
- tonyedgecombe 6y agoI assumed it was performance, my understanding is it keeps a stack of statements to execute a function end. Perhaps the cost was too high to do that at the end of each block.
- unscaled 6y agoThis is not about (runtime) performance. Like the the article mentions, the block-end version lets you implement everything without dynamic allocations and (I guess) pointer de-reference. The C3 implementation of block-end defer described in the article is zero-cost, as far as I can see. I suspect the real reason is the driving force behind a great deal of the early Go 1.x design choices: making the compiler simpler. Or, if you wish: _compiler_ performance. Go 2.x is shaping up to be a different beast, but Go 1.x had an extremely strong design philosophy favoring compiler simplicity (which often equates to compilation speed) over nearly every other concern, including runtime performance. That's exactly one of the main argument that was used against having generics: (non-erased) generics significantly improve performance, but slow down the compiler. I'm personally very much in disagreement with this philosophy, but I don't think all these design choices are baffling or even misguided - they clearly achieve what they were set to do.
- sltkr 6y agoI think the reason is much simpler: block-scoped defer isn't useful if you statements appear in a nested construct. Example: fd := OpenSocket(...); if fd != 0 { defer CloseSocket(fd); } // do stuff For the same reason, Go's defer cannot be implemented without dynamic allocations. For example, how would you implement: func foo(int n) void { for i := 0; i < n; i++ { var res = bar(n); defer fmt.Println(res); } } This calls bar(0), bar(1), etc. then prints the results of bar(n - 1), bar(n - 2), etc. There is no way to implement this without some way to store the intermediate values.
- ttt0 6y agoCompiler should be able to figure out at compile time whether the dynamic allocation is needed. But maybe the Go compiler already does this, I don't know.
- orisho 6y agodefer also is used to recover from panics, which makes much more sense in a function end context. For example, when writing code that performs an operation with the potential to panic, but that you want to convert to an error, something you can do is name the error return value and then modify it in the defer, after recovering from a panic, according to the cause of the panic. It's not obvious that that pattern would work well if defer could also execute on scope end - what does recover do then? Will it affect the following recovers? Also, if the function continues executing but you modified some variables, it makes it complicated to reason about what could be in those variables. Defer running at function end is much more easier to reason about: functions execute their standard control flow until a return or panic is hit, and then defers are executed, without returning to standard control flow.