This is true for practically every programming language, but particularly in Go, where the programmer is encouraged to return up the stack.
Calling functions should eventually handle the error - how do I eliminate the tight coupling?
In another language I might use a derived exception object that contains its own methods. Interfaces could potentially do this in Go but it probably would be very messy and not idiomatic.
The closest I found is https://blog.golang.org/error-handling-and-go but he says not to return the type of an error in a function signature for reasons I do not entirely understand.
评论:
Femaref:
Using the
error
interface does not mean you lose type information. In the end, an error is any type that implements theerror
interface (implementsError() string
method).Using an example from the link:
if err := dec.Decode(&val); err != nil { if serr, ok := err.(*json.SyntaxError); ok { line, col := findLine(f, serr.Offset) return fmt.Errorf("%s:%d:%d: %v", f.Name(), line, col, err) } return err }
The type assertion in line two allows you to handle errors in a specific way. The same is done in
os
with*os.PathError
,net
with*net.OpError
. I do it the same way for errors that need to carry more information. If there are very specific errors, you can use a package variable to compare against.database/sql
does this withsql.ErrNoRows
. Just create the error witherrors.New
to produce a descriptive error.Why not have the error in the function signature? You make it harder for yourself and the caller. For yourself, because you are restricted to that one error type. You can't just pass through an error that is out of your control, e.g. trying to open a file that doesn't exist. You have to shoe-horn every error case into your new error type.
You make it harder for the caller as you can't:
- reuse your error variable, if the error doesn't implement
error
- easily log the error as it doesn't implement
error
- type assert as the value is not an interface, you lose the actual type of the error and associated info
in short: it makes everything harder. The error
interface is the sane default case, and as it's an interface, you can still access the actual error by type asserting.
TheMerovius:Other redditors have commented, widely on a variety of other things. But something in particular jumped out at me:
particularly in Go, where the programmer is encouraged to return up the stack.
In Go, the programmer is encouraged to handle errors. Returning up the stack isn't particularly great for a huge number of usecases.
ui7_uy8:Calling functions should eventually handle the error - how do I eliminate the tight coupling?
In my opinion: You can't. All you may be able to do, is to remove compiler checks verifying that you are coupling correctly, but the coupling itself is intrinsic.
Say, for example, you are using a package
foo
, that stores some state in a file. Any errors encountered are passed on to you (potentially wrapped in some way). At some point, that package gets a feature to read the state from the network instead, say by making an HTTP request to etcd. Suddenly, all the possible error classes completely change - previously, you would get e.g. an*os.PathError
, now you suddenly get a*something.HTTPError
. When switching to the new feature, your code breaks, because you type-switched on*os.PathError
, which will no longer be returned, but the compiler was happy (because it got anerror
and*os.PathError
satisfies theerror
interface).From this POV, errors and their types are inherently API surface. Any concrete type that
foo
returns and that you rely on becomes part offoo
s API. But the underlying errors (in this example*os.PathError
and*something.HTTPError
) thatfoo
encounters are implementation details and shouldn't have been exposed in the API without careful consideration.As a consequence, if you rely on any feature of an
error
except it'sError
method, you are either a) binding yourself to implementation details, which are bound to change and won't be compiler checked or b) are relying on documented, stable APIs, in which case it is fine to couple strongly. Just as it's fine to couple yourself strongly to the signature of some exported function of a different package.A corollary of this is, that packages should spend just as much time thinking about their error API, as they should spend on all the other APIs. And find the right tradeoff between hiding implementation details and providing the flexibility and useful information to handle the errors they return. Personally, I tend to start with "expose no information but the error interface" and then refine by adding types, once the need to distinguish errors arises.
tv64738:but he says not to return the type of an error in a function signature for reasons I do not entirely understand.
I guess he is just saying return error interface instead of whatever concrete error type you have defined?
Remember that "go errors" are not a special features of the language, it's just a basic interface. It doesn't do anything special, it doesn't give you a stack or anything, it's like returning error codes like in C. An integer is nothing special.
It's a convention, not a "feature".
SeerUD:Here's an example of a looser coupling between the thing generating the error and the thing handling it: https://golang.org/pkg/net/#Error
The caller doesn't have to care what the exact error was, but if
err, ok := err.(net.Error); ok && err.Temporary()
then retrying is a good idea.
Using interfaces is fine, because you can test for behaviour instead of specific types. To eliminate the tight coupling, you can present new errors, or at least bubble up errors with some specific behaviour (i.e. not a type) and handle it later by testing the behaviour.
