5 ms·
On the topic of "syntax is important" (though not related to generics -- other than by aside to the Result type) I'm still not a huge fan of the handle syntax -
by cyphar 8y ago
On the topic of "syntax is important" (though not related to generics -- other than by aside to the Result type) I'm still not a huge fan of the handle syntax -- I reckon something more similar to Rust's .map_err() would be handled better because lots of people now use pkg/errors[1] which has the boilerplate of
if err != nil {
return errors.Wrap(err, "some message that is unique to this line")
}
and the handle construct doesn't really handle (excuse the pun) this properly (you have to re-define handle each time or have a local variable or something similar). Which means that the new construct won't actually be massively helpful unless the only thing you want to do is errors.WithStack or just bubble the error back up.
But I do get why they couldn't get .map_err() -- because that's something you can only really do if you have a Result type.
[1]: https://github.com/pkg/errors https://github.com/pkg/errors
- ape4 8y agoC/C++ __LINE__ is unique to a line.
- cyphar 8y agoI'm not sure you understand what I mean -- the "unique to a line" message is actually a descriptive message not a line number. errors.WithStack gives you the file, line, and function name for free and works with handle -- my complaint is that giving descriptive and stacked error messages will be very hard with handle (but is not fundamentally hard with Rust's map_err even though their error packages aren't as well-developed as Go's from my experience).
- Someone 8y agoNo, it isn’t, not even within a single file. https://gcc.gnu.org/onlinedocs/cpp/Line-Control.html https://gcc.gnu.org/onlinedocs/cpp/Line-Control.html: ”If you write a program which generates source code, such as the bison parser generator, you may want to adjust the preprocessor’s notion of the current file name and line number by hand. […] #line linenum linenum is a non-negative decimal integer constant. It specifies the line number which should be reported for the following line of input. Subsequent lines are counted from linenum. #line linenum filename linenum is the same as for the first form, and has the same effect. In addition, filename is a string constant. The following line and all subsequent lines are reported to come from the file it specifies, until something else happens to change that. filename is interpreted according to the normal rules for a string constant: backslash escapes are interpreted. This is different from ‘#include’.”
- ainar-g 8y agoYeah. I always try to include some context with my errors, so the only actual uses for check/handle I have is handle err { log.Fatal(err) } in quick "scripts". And even there I should probably provide more context. All clean-up work is already done in defer. Overall, check/handle feels like a poorly-thought slap-on to me, on par with ON-units from PL/I[1]. And PL/I is not something you want as an inspiration. [1] https://en.wikipedia.org/wiki/PL/I#ON-units_and_exception_handling https://en.wikipedia.org/wiki/PL/I#ON-units_and_exception_ha...
- cyphar 8y agoRight -- people already use defer as a way of mapping errors and doing cleanup in an already awful way: func something() (Err error) { defer func() { if Err != nil { someCleanup() Err = fmt.Errorf("wrapping: %v", Err) } }() return blah() } And unlike handle you can actually give an arbitrary mapping function, while as far as I can tell you'd need to do something like handle err { return mapError(err) } Rather than the more ergonomic handle mapError Or handle func(err Error) { return mapError(err) } But whatever -- all of this is pretty useless. The main benefit of check in my mind is actually that you would be able to easily bump your test coverage -- because Go test coverage is based on lines and so the repeated use of if err != nil { return err } artificially dilutes your test coverage (you don't need to test that every error is propagated -- that's just doesn't make any sense and might not be possible if you are returning directly from os.* functions that don't even have fault-injection).
- ljm 8y agoI enjoy the Go authors' attempts to propose novel or innovative solutions to existing problems, without going straight towards the status quo, but I do wonder if sometimes they're trying too hard. At risk of cargo culting or not knowing enough about the problem space, I can only think of what I've recently read from The Psychology of Computer Programming^0. The entire reason that generics and error handling are difficult is because of the decision to avoid a syntax table. This to me feels like an artificial constraint because the Go authors are optimising for compiler simplicity and not developer ergonomics. So the developer has to parse a whole lot more because the compiler guys don't want to take a hit. The reason why generics are such a pain in the ass, and errors are such a pain in the ass, is because the emotional attachment to how Go currently runs and what it stands for is still far more powerful than any attempt to innovate on the language's own status quo. So what Cheney says makes some sense in that in a lot of ways, it feels more intuitive than implementing language grammar that isn't executed but looks like real code. But at the same time, the whole debate is basically around how to change go without changing it. Either do it or don't. [0]https://leanpub.com/thepsychologyofcomputerprogramming https://leanpub.com/thepsychologyofcomputerprogramming
- pdeuchler 8y agoAs someone who just went to gophercon I'm still confused why they're tackling error handling first, and then generics. I would imagine tackling errors in Go 2.1 with the help of generics would make for a much much cleaner solution... if we even need one (i'm not fully convinced verbose error checking is as bad as people say it is)
- cyphar 8y ago(I agree that waiting until generics were ironed out would have been a better idea, because we already have pretty large communities around existing error handling libraries. Unfortunately this is a pattern of the Go language designers -- they have often ignored community consensus around an issue, like they did with vendor/ and other similar issues). My main issue with it (aside from making memes about RSI) is that it actually dilutes your test coverage artificially. 'go tool cover' instruments line-by-line (technically I think it's statement-by-statement but that's not relevant here) and so having a dummy line like if err != nil { return err } where you aren't doing anything useful with the error decreases your test coverage over a line which is not only obviously correct but might also be impossible to test (some APIs have an error return even though they can never fail in most usecases -- and as a user of the library it's better to be safe and check the error anyway). A perfect example of this is the Read() interface from math/rand -- it is impossible for this to return an error and yet you definitely should check the return value from Read() and it would be irresponsible not to.
- slavapestov 8y agoThe problem you describe is not specific to Go. You should always test all error paths in production code. Using mocks or error injection can help with triggering otherwise “impossible” cases.
- cyphar 8y agoThat is definitely one argument (and I do see where you're coming from -- especially if your error path has a defer that does cleanup or something complicated like that). However, I don't think spending a significant amount of time mocking out every struct method that has an error return is a worthwhile investment -- the majority of 'if err != nil { return err }' cases are not going to be interesting and the ones that are usually don't even need mocking to test because they are significant enough error cases that they are easy to trigger using the real version of whatever struct. And of course you should test error paths to make sure that your code does validate things.