4 ms·
The author seems to generalise his use case into a description of how to use panic/recover as a bespoke exception mechanism. In that case this is still bargaini
by lloeki 8y ago
The author seems to generalise his use case into a description of how to use panic/recover as a bespoke exception mechanism. In that case this is still bargaining.
> In imgproxy, I use this approach to stop image processing if the timeout is reached (see here, and here). The goal is not to bother about returning timeout errors from each function
(emphasis mine)
While I understand the pain, I do not like the train of thought. It is our duty as responsible developers to bother with error handling. Luckily this is not a library but a "standalone application", not a "library", which is a slightly different use case (e.g negroni has to recover from panics to log and continue serving http requests).
The accepted solution looks like† a world of pain waiting to blow up in a myriad of subtle corner cases (do you remember what happens exactly when there's a panic in a defer that was called because of a panic, and in which order deferred functions are called?) and is barely more readable. I've seen much more interesting and obviously robust Go 1 patterns (that I can't find right now) that e.g pass error handler functions around.
† At first sight. Maybe it's not when digging deeper, but that's not the point: the most glorious thing to me when I read idiomatic Go code is that basically everything is boringly obvious. This is a clever hack and crosses a threshold I'm not willing to go past in production-class code.
- jrockway 8y agoI completely agree. I think people are shocked, at first, at how everything can go wrong in ways they've never considered. Exceptions hide the complexity by pretending that errors are exceptional things that will never happen to YOU. The opposite is what's true, though, random errors will happen all the time; networks drop packets, remote machines are slow, database transactions fail because of other writers, etc. If you are aware of all the things that can go wrong, you can handle them. If you treat them as "exceptions" rather than the rule, then you will just write flaky software that needs constant manual attention. Some people like this, I guess, but I'm personally not a fan. The syntax could be better (I do have err, !=, and nil keys on my keyboard, I really do) and it would be nice to annotate errors and not lose the semantic information (fmt.Errorf("trying foo: %v", err) throws away the specifics of err beyond the result of err.Error()), but really... explicitly handling errors after every function, even if it's just "return err" and punt to the code one level up... is something you have to do in every language. Go just front-loads it. As for timeouts, I am not sure why you wouldn't just pass in a context, which already has provisions for a deadline, explicit cancellation, and cancelling further library calls. If you can cancel your operation when <-ctx.Done() returns, then you can ensure that all the related work is cancelled. You don't need a clever panic/recover for that. Just select { case <-workDone: ...; case <-ctx.Done(): cancelWork(); return ctx.Err() }. I am not sure why people rely on hacks when there's already a standard method built into the language.
- candiodari 8y agoThe problem with this attitude is that it ... it's just not true. C-style not true. C, like Go, convinces people they handle errors. And then, when you look into what that actually means, correctly handling error cases for even trivial problems ... you immediately find out "nope, you're not handling errors, you're ignoring them". C, and Go are very sneaky that way. 1) one of the more common error cases is nil, closely followed by out of memory and zero division ... which in Go causes a panic just like every other language. So your Go error handling in this case is never going to be executed. If you think you can avoid exceptions, you're lying to yourself. Every last method needs to be ready that a panic gets called at almost any point. Which ... is exactly what the error system was supposed to fix. 2) "If you treat them as "exceptions" rather than the rule, then you will just write flaky software ..." This sounds nice until you look at Go sources on github. And you see WHY they're not flaky. Do people handle errors more in Go sources than in Java ? The reverse is true ! But Go software is more stable. How can this happen ? Well, Go defaults to ignoring errors and just continuing whereas Java (and every sane language) defaults to aborting the program rather than letting it run in an unknown state. To put it more extreme, and more to the point Go just starts "sudo rm -Rf /" when an error happens. The reality is that Go programs just start writing things to the database based on wrong information when an error happens. 3) "As for timeouts, I am not sure why you wouldn't just pass in a context, which already has provisions for a deadline, explicit cancellation, and cancelling further library calls..." Again, this sounds cool. You can recognize code that actually uses this correctly. Incorrect (in more ways than one): if _, err = f.Write(something); err != nil { return err } More correct: var err error cont := true for retry := 0; cont; retry++ { if retry > max_retries { return fmt.Errorf("couldn't do X in max retries. Last error was: %v", err) } didit := make(chan bool) go func() { defer func() { if r := recover(); r != nil { err = fmt.Errorf("the call panicked: %v", r) } didit <- true; } n := 0 for n < len(something) { i, err = f.Write(something); if err != nil { return } n += i } } select { case <-didit: // Ok, go to next statement if err == nil { cont = false } case <-ctx.Done(): return fmt.Errorf("context signaled abort") case <-time.After(timeout): } } Have you seen such a code style ... even once ? And what will this do ? f.Write(something) One of 5 things: 1) it may actually work (one hopes, the common case) 2) it may not write anything 3) it may PARTIALLY write what you asked to be written (including not writing anything at all and returning EAGAIN, which is why the retry logic is not really optional. Frankly you should have a higher max_retries, and ignoring for the EAGAIN case to avoid some very rare circumstances) 4) it may panic 5a) it may block for a long time 5b) it may block forever (and even make your program unkillable. I mean, we've all used NFS, right ?) What will be your total amount of material to diagnose this problem when it occurs in cases 1-3 ? Nothing whatsoever. Case 4 ? A large collection of stacktraces. Thankfully usually with the relevant one on top. 5a and 5b ? 100 stacktraces, when you finally kill it (or ... well Go "supports hundreds of thousands goroutines"), one of which is relevant. Well the truth is that it may actually have so many goroutines that it ... well I managed to run out of diskspace once. But it's easily 10 megabytes and more in a real webserver.