The 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.