On Sun, Oct 23, 2022 at 1:08 AM Brian Candler <b.cand...@pobox.com> wrote:
> > And I agree that the above function is much easier to read and much > faster to write than the first version! But now I'm raising unchecked > exceptions instead of handling errors properly. > > However you're not "raising an unchecked exception"; you're panicking, > which is something different. Go does not encourage this at all. It would > be fine to write it that way if calling isGreen on a missing or non-color > key should never happen - i.e. you know that the programmer seriously > screwed up if it ever got this far. But it would almost never be > appropriate to attempt to recover() from such a situation. > When I say "unchecked exception" I mean in the Java sense of "an error condition that the compiler will not force you to address". The behavior of an unchecked exception is that it travels up the call stack, aborting each function until it either reaches the end of the stack and terminates the program, or reaches a call frame that indicates it will try to recover from it. This is exactly the behavior of panic - the only difference between `panic(e)` in Go and `throw e` in Java is that recovery in Java is written as "catch(Exception)" while in Go it's written as "defer func() { r := recover(); ... }()". Also exactly like in Go, most Java style guides A) discourage you from throwing unchecked exceptions, and B) agree that it is almost never appropriate to attempt to catch and recover from one. The only reason Go doesn't have severe problems with panic/recover being used everywhere for normal control flow while Java does is because in Java there's no immediate cost to just declaring your exception type to be a runtime exception, and it saves you some typing and shuts up some compiler errors, so people take that shortcut all the time, whereas in Go it's (correctly, in my opinion) a separate mechanism from checked errors (i.e. ones where it's a compiler error (usually) to forget to handle them). In practice, though, Go code *does* have a lot of unchecked exception usage - every time someone ignores the 'ok' on a type assertion, calls panic() or t.Fatal() from a helper function instead of returning an error, or writes a MustFoo() version of some function that panics instead of returning an error, that's a point where they *could* be using checked exceptions (i.e. return values), and have chosen not to because the code is easier to write. Regarding your sample code: it's OK but I can think of alternative APIs, > depending on how isGreen is intended to be used. > I said earlier that I didn't want to get bogged down on specific examples, and this is what I meant - I know that there absolutely are better ways to implement the specific example I gave, I was just trying to demonstrate that Go not only already allows "expressions that change control flow", but in fact has explicit special cases in the language in order to make it possible. The "isGreen" example was just to demonstrate this - I know that this particular example could be reimplemented to sidestep the problem, but I've seen code where this wasn't true (I didn't include any here because the examples I can think of are difficult to understand without reading the whole of a library first, and I obviously don't expect anyone to do that just so that I can make an example). Reflecting on this more, I think there are several distinct points that I'm arguing here. I'll try to make them explicit as I think them through: *1.* *There are places where streamlining error handling is acceptable and encouraged.* I don't mean that it's *always* good, or even "usually acceptable", just that cases do exist where it's desirable. The evidence for this is strong: * As I mentioned earlier, Go has special case syntax for certain operations like type assertions explicitly to streamline error handling. If streamlining errors was never a good idea, then Go would require you to write f, ok := x.(Foo) even if you know for a fact that x cannot be anything but a Foo, the way it currently requires you to write b, err := Bar(x) even if you know for a fact that x is a valid argument for Bar that won't cause an error. * In many cases, including in the standard libraries, there are functions that return errors and then accompanying functions with names like MustFoo() that just call Foo() and panic if there's an error. This is also error streamlining, and like with type assertions the streamlining comes at the cost of using panics. Checking sourcegraph.com suggests that hundreds of Go projects already choose to make this tradeoff: https://sourcegraph.com/search?q=context:global+lang:go++/func%5Cs%28%5C%28%5B%5E%29%5D%2B%5C%29%5Cs%29%3FMust/&patternType=standard (yes, some fraction of these are exclusively for module-level initialization, but at least half of the handful I randomly sampled were being called in other functions). * The Ondatra library I linked earlier <https://github.com/openconfig/ondatra/blob/main/dut.go#L113>, as well as other test helper libraries I've seen, use t.Fatal in the same way - they judged that "if got, want := ondatra.DUT(t, "name").Config().System().Hostname().Get(t), "localhost"; got != want { t.Errorf(...) }", which Fatals if there is no "name" entry in the DUT table, if the attempt to fetch the hostname fails, or if the hostname is unset, was still preferable to making people write out the fatal checks themselves every single time, even though it means that if you don't actually want this to terminate your test you have to workaround it with panics and recovers <https://github.com/openconfig/testt/blob/main/testt.go#L44>. *2.* *Streamlining doesn't have to mean using unchecked exceptions like panic() or t.Fatal().* All of the dozens of error handling proposals I've read so far are fundamentally aimed at letting you use the return value of a function that might return an error in contexts other than just assignment, and between them there are lots of different examples of errors that are more streamlined but still must be handled. My proposal is an example of one way to do this, but it is not the only way. *3. Streamlining shouldn't only apply to error handling that terminates the function.* Unlike panics, errors are values, and should be treated as such, which means that the calling function should be able to decide what to do, and this should include continuing. Frequently it won't - a lot of error handling is just return fmt.Errorf("more context %w", err) - but any proposal that assumes that it *always* won't is, IMO, confusing errors with panics. This is the question that first started this thread - I didn't understand why all the existing error proposals explicitly required that a function terminate when it encounters an error, and AFAICT the answer is "because people are used to thinking of errors more like panics than like return values". *4. The best way to have people write high-quality code is to make it easier to write high-quality code.* Nobody should ever have to look at their code and think "My API would be easier to use in normal circumstances if I just panicked in abnormal ones..." and then have to decide whether it's more important that their callers have a streamlined API or a principled one. This is not hypothetical - I talked to some of the engineers behind the Ondatra project that I linked above, and they went through exactly this, where they wanted to return errors but their users all felt it made the tests too verbose, and that's why they use t.Fatal() everywhere. I didn't start this thread with the intent of creating a new error handling proposal - I just wanted to understand why all the existing proposals don't care about point 3. But the more I think about this, the more I'm surprised it hasn't already been proposed. Is part of the problem that the discussion around the try/check/handle/etc. proposals got so involved that nobody wants to even consider anything that looks too similar to those? Would it be more palatable if I proposed it with names that made it clearer that this is about the consolidation of error handling rather than an attempt to replace it entirely? onErrors { if must Foo(must Bar(), must Baz()) > 1 { ... } } on err { ... } Thanks, Dan -- You received this message because you are subscribed to the Google Groups "golang-nuts" group. To unsubscribe from this group and stop receiving emails from it, send an email to golang-nuts+unsubscr...@googlegroups.com. To view this discussion on the web visit https://groups.google.com/d/msgid/golang-nuts/CAAViQtiXBHsVg4r-aTWR1mQWDX_7eRfhtPOKZ_YaNiP2ai7iSw%40mail.gmail.com.