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.

Reply via email to