Hacker News new | ask | show | jobs
by TheDong 1542 days ago
> have you ever actually done that? I have, its not easy.

Yes. I do it frequently. "#[derive(Error, Debug)]": https://github.com/dtolnay/thiserror#example

Much easier than implementing the error interface in go.

Rust is powerful enough to allow macros to remove annoying boiler-plate, and so most people using rust will grab one of the error-handling crates that are de-facto standard and remove the minor pain you're talking about.

In go, it's not really possible to do this because the language doesn't provide such macros (i.e. the old third-party github.com/pkg/errors wanted you to implement 'Cause', but couldn't provide sugar like 'this-error' does for it because go is simply less powerful).

I've found implementing errors in go to be much more error-prone and painful than in rust, and that's not to mention every function returning untyped errors, meaning I have no clue what callers should check for and handle new errors I add.

1 comments

> Much easier than implementing the error interface in go.

is this a joke? You have to import a third party package, just to implement an error interface? Here is Go example, no imports:

    type errorString string

    func (e errorString) Error() string {
       return string(e)
    }
It was not a joke.

Let's look at a common example: you want to return two different types of errors and have the caller distinguish between them. Let me show it to you in rust and go.

Rust:

    #[derive(Error, Debug)]
    pub enum MyErrors {
       #[error("NotFound: {0}")
       NotFound(String),
       #[error("Internal error")]
       Internal(#[source] anyhow::Error),
    }
The equivalent go would be something like:

    type NotFoundErr struct {
        msg string
    }

    func (err NotFoundErr) Error() string {
        return "NotFound: " + err.msg
    }

    func (err NotFoundErr) Is(target error) bool {
        if target == nil {
            return false
        }
        // All NotFoundErrs are considered the same, regardless of msg
        _, ok := target.(NotFoundErr)
        return ok
    }

    type InternalErr struct {
        wrapped error
    }
    
    func (err InternalErr) Error() string {
        return fmt.Sprintf("Internal error: %s", err.wrapped)
    }

    func (err InternalErr) Unwrap() error {
        return err.wrapped
    }
I dont think you realize how ridiculous this comment is. Youre comparing 10 lines of Go, with 200 of Rust:

https://github.com/dtolnay/thiserror/blob/master/src/lib.rs

What's the difference between importing some hundred's of lines from thiserror in rust vs importing the "error" package in go?

If the difference is just "It's okay to use stdlib code, but not dependencies", then go's a great language by that metric.

I don't think that's what matters at all. What matters is the abstraction that the programmer deals with.

In go, the abstraction I deal with, as a programmer, is what I showed above.

In rust, the abstraction I deal with is also what I showed above. One of those things is simpler.

Further, the abstraction in go is leakier. My function returns a 'error' interface even if it can only be those two concrete types, so the caller of my function has to step into my function, reason through it to figure out all the error variants, and then check them with 'errors.Is' or such, and changes to what variants are returned aren't statically checked.

In rust, I can look at the type-signature, write a 'match' statement, and never have to manually figure out all the variants returned, since the compiler just knows.

My point here is that what matters is the abstraction that programmers interact with. Third party libraries are bad when they give a leaky abstraction that causes the programmer pain. Standard libraries likewise.

In this case, the abstraction available in rust is cleaner and requires less code for me, the user of the package, so I don't think the lines of code used to build that abstraction matter.

Why do you see this as something that matters?

> What's the difference between importing some hundred's of lines from thiserror in rust vs importing the "error" package in go?

Again, you're comparing apples and oranges. It seems you didn't see my previous example, here it is again:

    type errorString string

    func (e errorString) Error() string {
       return string(e)
    }
I addressed that with the start of my comment: "Let's look at a common example: you want to return two different types of errors and have the caller distinguish between them"

Yes, your example implements the error interface, but it's not realistic. There is real go code that does that, but that's the reason I have to do string-matching type error handling in go, and frankly it's an argument against the language that its error handling is such a mess that some people see that as reasonable.

Having code that does

    return errorString("You have to do string matching on me internal error")
is something you can do, sure, but it's not good code. Idiomatically in go, it would also be "errors.New", not this errorString type you made.

In rust, that would also be just as easy since it would be:

    bail!("oh no internal error")
using anyhow, which is the de-facto standard way to do crappy string-style erroring, like the above.

But that's not what I want to talk about since that sort of error handling isn't interesting in either language, and isn't where they differ.

You've did notice the file you've linked to only has 7 non-comment/documentation lines, right?

Now, to be fair, there are 50 or so lines of code in other files, but I would still love to see a useful Go package which is that small.