dean
Posted on September 3, 2018
Go 2 is being drafted.
If you haven't heard, there will be a few major language changes coming to Go 2
. The Go team has recently submitted a draft of these changes, and as part of a 3-part series, I've been going through each part, explaining it, and state my opinion on them! This is the second part of the series. Last time I did error handling, and this time I will be discussing error values!
Remember that these are just drafts. They will most likely be added to the language, but there is still a chance that it won't. Also, remember that these drafts are not final, of course. The syntax will probably be a bit different from what it is right now.
These may not also be the only changes to the language, but these will probably be the only major changes.
I thought you already discussed errors!
Well, the first part was about error handling, and a new syntax which aides in handling errors. This part is about the improvements to the error
interface and the errors
package. There's some really good improvements that may be coming!
Okay, so what's wrong with the errors package?
There's two main issues: Inspecting errors, and Formatting errors.
Inspecting errors
Currently, there is no way to get context for an error without a custom implementation. So there's no provided way to provide context to an error you are returning, or to see if there was a cause for an error that you've received.
To fix this, the following may be defined in the errors
package:
package errors
// Describes an error which wraps another error
type Wrapper interface {
Unwrap() error
}
// Reports whether err or any of the errors in its chain is equal to target.
function Is(err, target error) bool
// Checks whether err or any of the errors in its chain is a value of type E.
// If so, it returns the discovered value of type E, with ok set to true.
// If not, it returns the zero value of type E, with ok set to false.
func As(type E)(err error) (e E, ok bool)
// NOTE this uses the syntax from the generics draft:
// https://go.googlesource.com/proposal/+/master/design/go2draft-generics-overview.md
Wrapper
is an interface which describes an error which wraps another error. This forms a singly-linked list (or "chain") of errors, with the head as the most general error ("could not initialize config") and the tail as the root cause of the error ("file not found").
Is
is the equivalent of the old if err == pack.ErrSomething
, but it checks the entire chain of errors. This way, you could do if errors.Is(err, os.ErrNotExist)
and it would tell you if at any point the error was caused by a file not existing.
As
is the equivalent of the old smth, ok := err.(pack.ErrSomething)
, but it also checks the entire chain of errors. Notice that the As
function uses generics, which I will cover in my next blog post.
Anyway, As
's usage would look something along the lines of errne, ok := errors.As(*os.ErrNotExist)(err)
. I personally don't like the double-parentheses syntax for generics (to be honest, I don't like generics that much in general...)
If generics are not yet in the language, a good substitute would be
var errne *os.ErrNotExist
errors.AsValue(&errne, err)
Formatting errors
Also, when viewing printed errors, there is often very little information! Printing the error gives something like "can't read config" or "invalid path ", and no good way to get more detail.
To address this, the following may be added to the errors
package:
type Formatter interface {
Format(p Printer) (next error)
}
Formatter
is an interface that describes an error which may be formatted. Printer
is an interface which contains the functions Print
and Printf
, along with a Detail
function. The Printer
is provided package that is printing the error (usually fmt if printing via fmt.Print...()).
Then, fmt.Errorf()
could then be improved to return an error implementing Formatter
. It is also suggested that if the format string ends with : %v
or : %s
that it should return a Wrapper
as well, thus improving the returned error even more.
Errors with custom formats could look like:
// Implements error and errors.Formatter
type ConfigError struct {
File string // A human-readable name for this config
Source string // The source file and line this error occurred on
CausedBy error // The error this was caused by
}
func (e *ConfigError) Init(file string, cause error) {
e.File = file
if _, file, line, ok := runtime.Caller(1); ok {
e.Source = fmt.Sprintf("%s:%s", )
} else {
e.Source = "error source unavailable"
}
e.CausedBy = cause
}
func (e *ConfigError) Format(p errors.Printer) (next error) {
p.Printf("error reading config %s", e.File)
if p.Detail() { // if printing with `%+v` rather than just `%v`
p.Printf("Perhaps incorrect file name or invalid format?")
p.Print(e.Source)
}
return e.CausedBy
}
func (e *ConfigError) Error() string {
return fmt.Sprint(e) // `fmt` will call the new Format function
}
And then when it's printed, it'll be something like...
fmt.Printf("%v", err)
error reading config MyConfig
===
fmt.Printf("%+v", err)
error reading config MyConfig:
Perhaps incorrect file name or invalid format?
github.com/deanveloper/config:1337
--- file does not exist
Nifty!
Putting these both together
Now, let's add one more function to our ConfigError
in order to complete it. Let's add our Unwrap
function so that we can use errors.Is
and errors.As
with it!
// Implements error, errors.Wrapper, and errors.Formatter
type ConfigError struct {
File string // A human-readable name for this config
Source string // The source file and line this error occurred on
CausedBy error // The error this was caused by
}
func (e *ConfigError) Init(file string, cause error) {
e.File = file
if _, file, line, ok := runtime.Caller(1); ok {
e.Source = fmt.Sprintf("%s:%s", )
} else {
e.Source = "error source unavailable"
}
e.CausedBy = cause
}
func (e *ConfigError) Format(p errors.Printer) (next error) {
p.Printf("error reading config %s", e.File)
if p.Detail() { // if printing with `%+v` rather than just `%v`
p.Printf("Perhaps incorrect file name or invalid format?")
p.Print(e.Source)
}
return e.CausedBy
}
func (e *ConfigError) Unwrap() error {
return e.CausedBy
}
func (e *ConfigError) Error() string {
return fmt.Sprint(e) // `fmt` will call the new Format function
}
And now we can use this error (note, I am using syntax from the "error handling" Go 2 proposal. I have a blog post on that one already)
func main() {
handle err {
if cfgErr, ok := errors.As(*config.ConfigError)(err); ok {
if errors.Is(err, os.ErrNotFound) {
// Nice, clean output if we couldn't find the file
log.Fatalln("Could not find config file %s", cfgErr.File)
}
}
// Hmm, let's get a more verbose output.
// Maybe a parsing error?
log.Fatalln("%+v", err)
}
check config.Create("MyConfig")
}
My Suggestions
In terms of errors.Is
, I'd really like a way to check multiple errors.
Perhaps instead of errors.Is(root, cause error) bool
we could have errors.IsAny(root error, causes ...error) error
(terrible name, but the semantics are what's important), which we could then use a switch err
on. It's very frustrating to not have an efficient way to check multiple types without looping ourselves.
Also, I think this is adding a lot of complexity to error values. To me, one of the things special about Go was that errors were simple. I like the Wrapper
idea, along with errors.Is
and errors.As
, but the Formatter
I think is a bit much. Maybe add a type Detailer interface { Detail() string }
to errors
and let fmt
take care of the actual formatting, as pretty much all Format
functions will be pretty similar...
func (e *MyErr) Format(p errors.Printer) (next error) {
p.Print(/* short error string */)
if p.Detail() {
p.Print(/* details */)
}
return /* caused by */
}
Wait a second, all the things I wrote placeholders for could just be functions!
If Go just had an errors.Detailer
interface, then our ConfigError
would look like...
// Implements error, errors.Wrapper, and errors.Detailer
type ConfigError struct {
File string // A human-readable name for this config
Source string // The source file and line this error occurred on
CausedBy error // The error this was caused by
}
func (e *ConfigError) Init(file string, cause error) {
e.File = file
if _, file, line, ok := runtime.Caller(1); ok {
e.Source = fmt.Sprintf("%s:%s", )
} else {
e.Source = "error source unavailable"
}
e.CausedBy = cause
}
func (e *ConfigError) Detail() string {
return "Perhaps incorrect file name or invalid format?\n" + e.Source
}
func (e *ConfigError) Unwrap() error {
return e.CausedBy
}
func (e *ConfigError) Error() string {
return "error reading config " + e.File
}
And then fmt
would be able to print each error in the chain.
All in all...
I think these are welcome changes, and my changes are a bit nitpicky. I'm fine with the Format
function, even though I think that just adding a Detailer
interface would be better. I will be making that as a suggestion to the Go team.
Posted on September 3, 2018
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.