Hacker News new | ask | show | jobs
by veber-alex 1073 days ago
> Go’s error handling is more verbose than those other languages, but structurally, there’s a lot of commonality under the surface.

In Go the type system doesn't force you to check for errors, the same way as languages with null pointers don't force you to check pointers before dereferencing them.

That's the real problem with errors and null in Go, not the verbosity (though that doesn't help)

1 comments

The typical answer is that Go doesn't allow you to not address values returned from a function. So, to ignore an error, you'd typically have to write:

result, _ := myfunc()

That being said, I most certainly prefer the approach that Rust takes to this problem.

  a, err := f()
  if err != nil {
      return
  } 
  a, err = f()
This will compile.
I consider use of the "errcheck" linter mandatory for a professional Go programmer, and honestly even the hobbiest really ought to be using it.

Yeah, it might be nice if it were integrated into the language but on the overall cost/benefits analysis of my actual costs & benefits rather than merely aesthetic ones, this one doesn't actually factor very high for me because using errcheck is easy. And I supplement all languages I use seriously with aftermarket checkers so it isn't like this is special pleading for Go, either. I don't trust any language out of the box any more.

It still doesn't catch everything.
I am not aware of an option that "catches everything", in any langauge.
Any language with exceptions, checked or unchecked, will not allow errors to unintentionally get swallowed unless you write explicit code to do so. Rust and Zig's error handling also has the same property.

This comes from experience working on large golang code bases, with error linters, and seeing errors silently and unintentionally ignored.

In terms of default, there are enough languages where all errors are detected and you are forced to handle them. But if you want a particular mean language, check Idris. No chance to ignore an error by accident.
I'm not at all defending the practice. I agree it's very easy to navigate around this. My answer is just the canonical one I've seen over and over again in books about Go.
It's not just that it's "very easy to navigate around this", it's that it's not true. The incorrect statement piggybacks on Go forbidding unused variables, however that has two absolutely major holes:

First, it requires having a variable in the first place, if you call a function for its side-effect and don't remember that it returns an error, Go won't tell you.

Second, Go only errors on dead variables, not dead stores, since conventionally the error variable is "err" you can easily forget to check one of them, and Go won't say anything because the err variable was read in one of the other checks.

It's even worse if you're one of the weirdoes which uses named return variables, because named return variables are always used:

    func foo() (v int, err error) {
        a, err := bar()
        b, err := baz()
        v = a + b
        return
    }
compiles just fine. But at least you're returning the second error. No such luck if you're using named return variables for documentation:

    func foo() (v int, err error) {
        a, err := bar()
        b, err := baz()

        return a + b, nil
    }
go also has nothing to say about this.
a isn't used so it won't ;p

Honestly the thing that I think lacks more is macros. With macros it would be trivial to write

   a := Must!(f())
that

* assigns last return value to err

* calls return with that error if it is not nil

    b, err := os.ReadFile(path)
    if err != nil {
      return nil, fmt.Errorf("read %s: %w", path, err)
    }
is so much better than

    b := Must!(os.ReadFile(path))
because when things go wrong, I have exactly the right amount of information I want. Assigning to err magically (it's not even mentioned in the source code) is exactly the kind of thing that'll turn out to be the cause of a subtle bug 6 months later. Why not spend the a couple of extra lines thinking about the error when the context is still in your head?

Additionally I like how error handling acts as a visual delimiter, especially when you use meaningful fmt.Errorfs as strings are usually highlighted with a different colour, making it easy to quickly jump through code.

> I have exactly the right amount of information I want.

Really? Because `ReadFile` already adds the path to the context, so what you actually get is

    read /tmp/foo: open /tmp/foo: no such file or directory
which is more confusing than "the right amount of information".

Furthermore nothing precludes `Must!` taking a prefix and wrapping automatically, does it?

> Assigning to err magically (it's not even mentioned in the source code) is exactly the kind of thing that'll turn out to be the cause of a subtle bug 6 months later.

What bug? It's assigning and returning, the only situation where you'd have "a subtle bug" is if you didn't check the previous call and overwrote its error, which is exactly what you get with the code you propose.

Macros are a lazy design cop-out. They subvert the entire point of having a language in the first place - a shared understanding.
If there is a single result you can also on ignore the return value by calling myfunc()