I'll offer a comparison to Rust here to contrast.

In Rust, errors are generally either a struct (to represent a single possible kind of error) or an enum of structs (to represent multiple potential underlying errors). These aren't C-style enums, they're sum types. So if your function returns a Result with an Error in it, that error is precisely one of those underlying struct types.

This has some enormous advantages. If the library author provided a way to convert their errors to a human-readable string, you can simply call that method and do so (similar to go's error interface). If they didn't or you would prefer to use your own string descriptions, the enumeration provides the complete list of possible error types to the compiler. So you can match (a.k.a. case or switch) on the error type and convert them to strings of your own choosing and guarantee statically at compile time that every possible error type is handled. You can use this same machinery to detect the error type and recover from ones you know how to handle or bubble up the ones you can't.

This is much more powerful and flexible than the Go equivalent and doesn't exactly come with much additional mental burden. With Go, the only thing you're promised is that your error type can be turned into a string, that's it. You can check that the errors are of a certain type, but there is no way to know at compile-time what all possible errors are. In fact, because most Go programs just use strings as errors directly (`fmt.Errorf("bad thing happened")`), the only types of errors you can generally detect and recover safely from are ones that happen in functions that can fail in precisely one way or functions that have only one possible way to recover from all their failure modes.

Of course, Go programs could implement error structs that you can switch on. But nobody in practice seems to do this. And even if they did, there are no compile-time guarantees that ensure you're covering every unique failure case. If the function you're calling adds a new error type, there's no way to know this other than to have an `else` that covers "everything I didn't know about".

Let's take one toy example: creating a file. This could fail because the directory doesn't exist. This could fail because we don't have permissions to write to the directory. In both of these cases maybe we want to fall back to an alternate location. Or it could fail because of something unrecoverable: your disk is full. In Rust, this is trivial to do. You can match on the error type, handle ErrorKind::NotADirectory, ErrorKind::NotFound, etc., and fallback. For anything else, bubble the error up. In Go, you get an error back. The docs promise that it's of type *PathError, but that isn't enforced by the compiler so you get to typeswitch. Even then that doesn't really help you much because fs.PathError is just

type PathError struct {
Op string
Path string
Err error
}

All you get about that internal error is that it's convertible to a string. So after typeswitching, now you could theoretically switch on `error.Err.String()` to do this but now you need to figure out every possible string that is returned for the error cases you want to handle. And of course, those strings could change in future updates or new ones could be added without you ever knowing.