The Go Blog
Error handling and Go
Andrew Gerrand
12 July 2011
Introduction
If you have written any Go code you have probably encountered the built-inerror type.Go code useserror values to indicate an abnormal state.For example, theos.Open function returns a non-nilerror value whenit fails to open a file.
func Open(name string) (file *File, err error)The following code usesos.Open to open a file.If an error occurs it callslog.Fatal to print the error message and stop.
f, err := os.Open("filename.ext")if err != nil { log.Fatal(err)}// do something with the open *File fYou can get a lot done in Go knowing just this about theerror type,but in this article we’ll take a closer look aterror and discuss somegood practices for error handling in Go.
The error type
Theerror type is an interface type. Anerror variable represents anyvalue that can describe itself as a string.Here is the interface’s declaration:
type error interface { Error() string}Theerror type, as with all built in types,ispredeclaredin theuniverse block.
The most commonly-usederror implementation is theerrorspackage’s unexportederrorString type.
// errorString is a trivial implementation of error.type errorString struct { s string}func (e *errorString) Error() string { return e.s}You can construct one of these values with theerrors.New function.It takes a string that it converts to anerrors.errorString and returnsas anerror value.
// New returns an error that formats as the given text.func New(text string) error { return &errorString{text}}Here’s how you might useerrors.New:
func Sqrt(f float64) (float64, error) { if f < 0 { return 0, errors.New("math: square root of negative number") } // implementation}A caller passing a negative argument toSqrt receives a non-nilerrorvalue (whose concrete representation is anerrors.errorString value).The caller can access the error string (“math:square root of…”) by calling theerror’sError method,or by just printing it:
f, err := Sqrt(-1)if err != nil { fmt.Println(err)}Thefmt package formats anerror value by calling itsError() string method.
It is the error implementation’s responsibility to summarize the context.The error returned byos.Open formats as “open /etc/passwd:permission denied,” not just “permission denied.” The error returned byourSqrt is missing information about the invalid argument.
To add that information, a useful function is thefmt package’sErrorf.It formats a string according toPrintf’s rules and returns it as anerrorcreated byerrors.New.
if f < 0 { return 0, fmt.Errorf("math: square root of negative number %g", f)}In many casesfmt.Errorf is good enough,but sinceerror is an interface, you can use arbitrary data structures as error values,to allow callers to inspect the details of the error.
For instance, our hypothetical callers might want to recover the invalidargument passed toSqrt.We can enable that by defining a new error implementation instead of usingerrors.errorString:
type NegativeSqrtError float64func (f NegativeSqrtError) Error() string { return fmt.Sprintf("math: square root of negative number %g", float64(f))}A sophisticated caller can then use atype assertionto check for aNegativeSqrtError and handle it specially,while callers that just pass the error tofmt.Println orlog.Fatal willsee no change in behavior.
As another example, thejsonpackage specifies aSyntaxError type that thejson.Decode function returnswhen it encounters a syntax error parsing a JSON blob.
type SyntaxError struct { msg string // description of error Offset int64 // error occurred after reading Offset bytes}func (e *SyntaxError) Error() string { return e.msg }TheOffset field isn’t even shown in the default formatting of the error,but callers can use it to add file and line information to their error messages:
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}(This is a slightly simplified version of someactual codefrom theCamlistore project.)
Theerror interface requires only aError method;specific error implementations might have additional methods.For instance, thenet package returns errors of typeerror,following the usual convention, but some of the error implementations haveadditional methods defined by thenet.Error interface:
package nettype Error interface { error Timeout() bool // Is the error a timeout? Temporary() bool // Is the error temporary?}Client code can test for anet.Error with a type assertion and then distinguishtransient network errors from permanent ones.For instance, a web crawler might sleep and retry when it encounters a temporaryerror and give up otherwise.
if nerr, ok := err.(net.Error); ok && nerr.Temporary() { time.Sleep(1e9) continue}if err != nil { log.Fatal(err)}Simplifying repetitive error handling
In Go, error handling is important. The language’s design and conventionsencourage you to explicitly check for errors where they occur (as distinctfrom the convention in other languages of throwing exceptions and sometimes catching them).In some cases this makes Go code verbose,but fortunately there are some techniques you can use to minimize repetitive error handling.
Consider anApp Engineapplication with an HTTP handler that retrieves a record from the datastoreand formats it with a template.
func init() { http.HandleFunc("/view", viewRecord)}func viewRecord(w http.ResponseWriter, r *http.Request) { c := appengine.NewContext(r) key := datastore.NewKey(c, "Record", r.FormValue("id"), 0, nil) record := new(Record) if err := datastore.Get(c, key, record); err != nil { http.Error(w, err.Error(), 500) return } if err := viewTemplate.Execute(w, record); err != nil { http.Error(w, err.Error(), 500) }}This function handles errors returned by thedatastore.Get function andviewTemplate’sExecute method.In both cases, it presents a simple error message to the user with the HTTPstatus code 500 (“Internal Server Error”).This looks like a manageable amount of code,but add some more HTTP handlers and you quickly end up with many copiesof identical error handling code.
To reduce the repetition we can define our own HTTPappHandler type that includes anerror return value:
type appHandler func(http.ResponseWriter, *http.Request) errorThen we can change ourviewRecord function to return errors:
func viewRecord(w http.ResponseWriter, r *http.Request) error { c := appengine.NewContext(r) key := datastore.NewKey(c, "Record", r.FormValue("id"), 0, nil) record := new(Record) if err := datastore.Get(c, key, record); err != nil { return err } return viewTemplate.Execute(w, record)}This is simpler than the original version,but thehttp package doesn’t understandfunctions that returnerror.To fix this we can implement thehttp.Handler interface’sServeHTTPmethod onappHandler:
func (fn appHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { if err := fn(w, r); err != nil { http.Error(w, err.Error(), 500) }}TheServeHTTP method calls theappHandler function and displays thereturned error (if any) to the user.Notice that the method’s receiver,fn, is a function.(Go can do that!) The method invokes the function by calling the receiverin the expressionfn(w, r).
Now when registeringviewRecord with the http package we use theHandlefunction (instead ofHandleFunc) asappHandler is anhttp.Handler(not anhttp.HandlerFunc).
func init() { http.Handle("/view", appHandler(viewRecord))}With this basic error handling infrastructure in place,we can make it more user friendly.Rather than just displaying the error string,it would be better to give the user a simple error message with an appropriate HTTP status code,while logging the full error to the App Engine developer console for debugging purposes.
To do this we create anappError struct containing anerror and some other fields:
type appError struct { Error error Message string Code int}Next we modify the appHandler type to return*appError values:
type appHandler func(http.ResponseWriter, *http.Request) *appError(It’s usually a mistake to pass back the concrete type of an error rather thanerror,for reasons discussed inthe Go FAQ,but it’s the right thing to do here becauseServeHTTP is the only placethat sees the value and uses its contents.)
And makeappHandler’sServeHTTP method display theappError’sMessageto the user with the correct HTTP statusCode and log the fullErrorto the developer console:
func (fn appHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { if e := fn(w, r); e != nil { // e is *appError, not os.Error. c := appengine.NewContext(r) c.Errorf("%v", e.Error) http.Error(w, e.Message, e.Code) }}Finally, we updateviewRecord to the new function signature and have itreturn more context when it encounters an error:
func viewRecord(w http.ResponseWriter, r *http.Request) *appError { c := appengine.NewContext(r) key := datastore.NewKey(c, "Record", r.FormValue("id"), 0, nil) record := new(Record) if err := datastore.Get(c, key, record); err != nil { return &appError{err, "Record not found", 404} } if err := viewTemplate.Execute(w, record); err != nil { return &appError{err, "Can't display record", 500} } return nil}This version ofviewRecord is the same length as the original,but now each of those lines has specific meaning and we are providing afriendlier user experience.
It doesn’t end there; we can further improve the error handling in our application. Some ideas:
give the error handler a pretty HTML template,
make debugging easier by writing the stack trace to the HTTP response when the user is an administrator,
write a constructor function for
appErrorthat stores the stack trace for easier debugging,recover from panics inside the
appHandler,logging the error to the console as “Critical,” while telling the user “aserious error has occurred.” This is a nice touch to avoid exposing theuser to inscrutable error messages caused by programming errors.See theDefer, Panic, and Recoverarticle for more details.
Conclusion
Proper error handling is an essential requirement of good software.By employing the techniques described in this post you should be able towrite more reliable and succinct Go code.
Next article:Go for App Engine is now generally available
Previous article:First Class Functions in Go
Blog Index
[8]ページ先頭