This blog investigates the error types in golang and summerizes a best practice for using errors.
Error types¶
Sential error¶
Sential error is an error we predefine and return it when we need it like this:
// this is an example of the sentinel error
var ErrNotFound = errors.New("not found")
// can be returned this way
return nil, ErrNotFound
// can be added with additional context in case needed
return nil, fmt.Errorf("some operation: %w", ErrNotFound)
Error type¶
- key: type assert and get data
Error type is define our own error type and implement the error
interface. This is usually used for carry some useful information for the callstack and expose error from package.
type SyntaxError struct {
msg string // description of error
Offset int64 // error occurred after reading Offset bytes
}
func (e SyntaxError) Error() string { return e.msg }
So when we get an error from one of functions in the package, we can use type assert to fetch the data inside it if it’s a predicted error type.
Opaque error¶
- key: type assert and decide workflow
Opaque error means we define our own error type, but not expose them. Instead, we expose the interfaces the error might implement. This mainly aims to decide the workflow and we don’t care the data inside the error.
- For example:
type temporary interface {
Temporary() bool
}
// IsTemporary returns true if err is temporary.
func IsTemporary(err error) bool {
te, ok := err.(temporary)
return ok && te.Temporary()
}
type timeoutError struct{}
func (e *timeoutError) Error() string { return "i/o timeout" }
func (e *timeoutError) Temporary() bool { return true }
type rateLimitError struct{}
func (e *timeoutError) Error() string { return "rate limited" }
func (e *timeoutError) Temporary() bool { return true }
// this error when checked by IsTemporary(), will always return false
// since it doesn't satisfy the temporary interface
type parseError struct{}
func (e *timeoutError) Error() string { return "parse error" }
The net package also define their own opaque error interface like this:
// An Error represents a network error.
type Error interface {
error
Timeout() bool // Is the error a timeout?
Temporary() bool // Is the error temporary?
}
Note that it’s better to use opaque error
when we have more than 1 case should be check, or using type error.
Pointer receiver or value receiver?¶
Error is an interface, all structs implement it could be an error type:
Basically, we will define our error type with a value receiver or a pointer receiver:
type valueError struct {
info string
}
func (e valueError) Error() string {
return e.info
}
type pointerError struct {
info string
}
func (e *pointerError) Error() string {
return e.info
}
Prerequisities knowledges¶
Value passed by interface is a reference¶
Interface values are represented as a two-word pair giving a pointer to information about the type stored in the interface and a pointer to the associated data.
func cleanupDir(storage StorageAPI, volume, dirPath string) error {
// [...]
entries, err := storage.ListDir(volume, entryPath)
// [...]
}
type magicI struct {
tab *_typeDef
ptr *retryStorage
}
func cleanupDir(storage magicI, ...) error {
// [...]
// we're trying to call (*retryStorage).ListDir()
// since what we have is a pointer, not a value.
entries, err := storage.ptr.ListDir(...)
// [...]
}
Compiler work for function receiver¶
For methods with value receivers, the compilers will apply a pointer method implicitly like:
But for the method with pointer receiver, compiler does nothing as:
The method set of the corresponding pointer type *T is the set of all methods declared with receiver *T or T (that is, it also contains the method set of T).
Better to use pointer receiver¶
We can always use pointer receiver for Error
method and benefit from no copy cost and no generated function cost.
If we see errors.New
:
func New(text string) error {
return &errorString{text}
}
// errorString is a trivial implementation of error.
type errorString struct {
s string
}
func (e *errorString) Error() string {
return e.s
}
And fmt.Errorf
: