7 ms·
Defer is a bad feature in Go and I'm hesitant to see it spread to other languages. It exists as a hack because error handling in Go is convoluted.
by ohCh6zos 5y ago
Defer is a bad feature in Go and I'm hesitant to see it spread to other languages. It exists as a hack because error handling in Go is convoluted.
- leni536 5y agoDefer is weird in Go, being tied to function scope instead of immediate block scope.
- ohCh6zos 5y agoYou're right, that is a better way to put it than what I said.
- assbuttbuttass 5y agoFunction scope allows constructs like for _, lock := range locks { lock.Lock() defer lock.Unlock() } To acquire locks in a loop and release them at the end of a function. Otherwise the previous lock would be dropped each time the loop executes. I've never seen the advantage to having a block structured defer: it's easy to add a new function, it's not always possible to remove a block.
- jcelerier 5y ago> To acquire locks in a loop and release them at the end of a function. that looks like a super big gotcha, hasn't there been enough bugs with alloca() being called in a loop yet to show how risky and unintuitive this is ? block-scoped is explicit and explicit is good
- kaba0 5y agoGood implementations of defer are scope-based, so those would be clean up at the end of the loop pretty explicitly.
- duped 5y agoNamed scopes would alleviate this problems, and I wish more languages would adopt them.
- dzaima 5y ago..Except that in C that'll turn into stack allocations and will easily segfault due to a stack overflow. So you can't actually use it like that in practice (unless you can guarantee your loop will be called a small number of times). You probably even won't notice while writing the code, and will just get random segfaults wherever you have a defer in a loop when you hit a large enough iteration count. Never mind it being very inefficient while it doesn't.
- assbuttbuttass 5y agoThat's a good point. I was wondering about Zig's defer, which has a block scope, and I suspect it's exactly because of this issue. Go can allocate defers on the heap, but that's a different story.
- gmfawcett 5y agoWell, we are earnestly analyzing a silly little example program, but okay :). The equivalent C code wouldn't produce stack-allocated mutexes unless that's what the programmer wanted. E.g. the POSIX pthread functions don't care where your mutexes are allocated, since they are always passed by reference.
- dzaima 5y agoIt's not the mutexes that'd be stack-allocated, but the list of things to call back to at the end of the function. The locks list could be modified or freed by the end of the function, but something still must hold the list of things to deferred-unlock. The pthread_cleanup_push/pthread_cleanup_pop thing presumably keeps its own heap-allocated vector, backed by malloc or something. C itself can't willy nilly heap-allocate, so that list will be on the stack. But the stack is tiny compared to how long loops can be. Hence stack overflow.
- gmfawcett 5y agoC libraries -- including the runtime implementations of features like this -- can heap allocate just fine. They just need to return pointers on the stack to the heap allocated values, either directly or indirectly (e.g. buried in a struct return value). As is always the case with C, the burden to free the memory is on the caller: no problem. Given a possible implementation, this loopy mutex example could have a tiny stack footprint: a single pointer to the shared cleanup() function; and a single pointer to the head of a (heap allocated) linked list of pointers to mutex (i.e., the function arguments). And the function pointer would not necessarily require allocation at all, as we can statically point at the function definition here. So we are down to a single word of stack allocation.
- gaganyaan 5y agoThat code is odd (though I assume idiomatic Go). I'd much rather have block scoping with something like this: for _, lock := range locks { lock.Lock() // Do something with lock } And I don't really do Go, but in Rust, if I wanted to lock some arbitrary list of locks for a whole function, it would just be something like this at the top: let _ = locks.iter().map(|l| l.lock().unwrap()).collect::<Vec<_>>();
- boardwaalk 5y agoHow often do you do something like that? Seems pretty rare. If you're managing locks maybe having an actual collection for them makes sense. If I had to choose between allowing that pattern and allowing reasonable usage in all control constructs, I'd choose the latter.
- Someone 5y agoI can see one use that when using lock ordering to prevent deadlocks (http://tutorials.jenkov.com/java-concurrency/deadlock-prevention.html#ordering http://tutorials.jenkov.com/java-concurrency/deadlock-preven...), but then, the locks to be taken are a fixed set, and one probably would have function take_locks(lock *) and release_locks(lock *), and one could do defer release_locks. Also, Wikipedia (https://en.wikipedia.org/wiki/Deadlock https://en.wikipedia.org/wiki/Deadlock, https://en.wikipedia.org/wiki/Deadlock_prevention_algorithms https://en.wikipedia.org/wiki/Deadlock_prevention_algorithms) doesn’t seem to know about it. Even though I expect/guess it to be popular in embedded work, that makes me wonder whether lock ordering is used much. Could also be an omission in Wikipedia. It has https://en.wikipedia.org/wiki/Banker%27s_algorithm https://en.wikipedia.org/wiki/Banker%27s_algorithm, which I hadn’t heard of, and that Wikipedia says of “In most systems, this information is unavailable, making it impossible to implement the Banker's algorithm. Also, it is unrealistic to assume that the number of processes is static since in most systems the number of processes varies dynamically”
- KerrAvon 5y agoI see this code and all I can think is that you’re going to be spending the rest of your life debugging threadsafety issues. I can contrive a case where this is useful, but where in the real world?
- pcwalton 5y agoThe semantics of block-scope defer are much simpler, easier to understand, and faster. The compiler always statically knows which defers execute at any given point, which aids optimization. With function-scope defer, the semantics are extremely dynamic, requiring bookkeeping of a runtime stack of defer thunks, and compilers have a hard time optimizing it. Function-scope defer is something that surprises everyone I explain it to. Programmers naturally expect defer to be block-scoped.
- kgeist 5y ago>With function-scope defer, the semantics are extremely dynamic, requiring bookkeeping of a runtime stack of defer thunks, and compilers have a hard time optimizing it. As far as I know, most functions have only one defer, and Go optimizes such cases quite trivially, by inlining calls to the deferred function at every function exit at compile time, without managing an additional stack. If there are several defers, then yes, the slow path is used.
- Thaxll 5y agoYou mixed up the two I guess?
- bboozzoo 5y agoWhat in your view makes defer a bad feature of Go? Maybe my bar is low, but each time I jump back to C I wish I had defer and end up abusing __attribute__((cleaup)) instead.
- deleted 5y ago[deleted]
- deleted 5y ago[deleted]
- SV_BubbleTime 5y ago> error handling in Go is convoluted Is it worse than in C that there is no concept at all? Is it better that everyone is on their own to do it their own way every time? I’m not above making the eaiser, but to your point when I see their example of: double* q = malloc(something); defer [qp = &q]{ free(*qp); }; That doesn’t look like the C that I know.
- pjmlp 5y agoThe C you will get to learn is also planning to have lambdas, and they are being based on C++ design rather than Apple's blocks, so the example assumes they will also be in C2X.
- halpert 5y agoBeing able to close an open resource when a function returns is generally useful. It has nothing to do with error handling.
- kgeist 5y ago>It exists as a hack because error handling in Go is convoluted. What's the alternative, though? Sprinkle your code with try..finally's? RAII like in C++?
- the_gipsy 5y agoResult types
- qsort 5y agoContext managers, or an Auto-Closable interface with syntactic support. Also why is RAII bad? It's an awesome feature in C++.
- kgeist 5y ago>Also why is RAII bad? It's an awesome feature in C++ I didn't mean it's bad (I used to be a C++ developer myself and enjoyed RAII a lot), just wondering what are the alternatives that the OP doesn't consider "hacks". RAII would require to introduce constructors/destructors in the language, with all the gotchas (and probably you'll want a full-fledged OOP after that), which is apparently against Go's design principles as a simple language. >Auto-Closable interface with syntactic support. I don't see much difference here in practice; the whole difference is that in C#, for example, you use "using" on a whole object, while in Go it's a "defer" on a specific method of the object (or a standalone function). You are not limited to a single method and can use it on any method you deem necessary. Auto-closeable/RAII, however, is less flexible in ad hoc situations specific to a certain function (you have to define dummy classes just to make sure a function is called no matter what), Go allows to use "defer" on a lambda. Auto-closeable also ties control flow to an object, which makes sense in an OOP-focused language, but Go isn't one.
- laumars 5y agoIt’s already exists in other languages and it exists because a function could have multiple different exit points. Even if Go had different error handling there might well be different exit points. Hence why defer isn’t a Go-specific feature.
- Thaxll 5y agoDefer has nothing to do with error handling. Defer is a great feature in Go, I've never seen someone complain about it.
- laserbeam 5y agoDefer CAN help with error handling, but you need other language features for that. You need well defined error types or exceptions in some way or another. Zig has errdefer which only runs if an error occurred below the statement within that scope. It allows you to always keep cleanup locally, but you can still handle errors for your business logic somewhere else. errdefer is cool, but I don't think C should support it. Mainly because you'd need to spec errors at a language level, and doing that today is probably impossible.