11.1 Error handling

Go's major design considerations are rooted in the following ideas: a simple, clear, and concise syntax (similar to C) and statements which are explicit and don't contain any hidden or unexpected things. Go's error handling scheme reflects all of these principles in the way that it's implemented. If you're familiar with the C language, you'll know that it's common to return -1 or NULL values to indicate that an error has occurred. However users who are not familiar with C's API will not know exactly what these return values mean. In C, it's not explicit whether a value of 0 indicates success of failure. On the other hand, Go explicitly defines a type called error for the sole purpose of expressing errors. Whenever a function returns, we check to see whether the error variable is nil or not to determine if the operation was successful. For example, the os.Open function fails, it will return a non-nil error variable.

func Open(name string) (file * File, err error)

Here's an example of how we'd handle an error in os.Open. First, we attempt to open a file. When the function returns, we check to see whether it succeeded or not by comparing the error return value with nil, calling log.Fatal to output an error message if it's a non-nil value:

f, err := os.Open("filename.ext")
if err != nil {
  log.Fatal(err)
}

Similar to the os.Open function, the functions in Go's standard packages all return error variables to facilitate error handling. This section will go into detail about the design of error types and discuss how to properly handle errors in web applications.

Error type

error is an interface type with the following definition:

type error interface {
    Error() string
}

error is a built-in interface type. We can find its definition in the builtin package below. We also have a lot of internal packages which use error in a private structure called errorString, which implements the error interface:

// errorString is a trivial implementation of error.
type errorString struct {
    s string
}

func (e *errorString) Error() string {
    return e.s
}

You can convert a regular string to an errorString through errors.New in order to get an object that satisfies the error interface. Its internal implementation is as follows:

// New returns an error that formats as the given text.
func New(text string) error {
    return &errorString{text}
}

The following example demonstrates how to use errors.New:

func Sqrt(f float64) (float64, error) {
    if f < 0 {
        return 0, errors.New("math: square root of negative number")
    }
    // implementation
}

In the following example, we pass a negative number to our Sqrt function. Checking the err variable, we check whether the error object is non-nil using a simple nil comparison. The result of the comparison is true, so fmt.Println (the fmt package calls the error method when dealing with error calls) is called to output an error.

f, err := Sqrt(-1)
if err != nil {
    fmt.Println(err)
}    

Custom Errors

Through the above description, we know that a go Error is an interface. By defining a struct that implements this interface, we can implement their error definitions. Here's an example from the JSON package:

type SyntaxError struct {
    msg string // error description
    Offset int64 // where the error occurred
}

func (e * SyntaxError) Error() string {return e.msg}

The error's Offset field will not be printed at runtime when syntax errors occur, but using a type assertion error type, you can print the desired error message:

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
}

It should be noted that when the function returns a custom error, the return value is set to the recommended type of error rather than a custom error type. Be careful not to pre-declare variables of custom error types. For example:

func Decode() *SyntaxError {
    // error, which may lead to the caller's err != nil comparison to always be true.
    var err * SyntaxError // pre-declare error variable
    if an error condition {
        err = &SyntaxError{}
    }
    return err // error, err always equal non-nil, causes caller's err != nil comparison to always be true
}

See http://golang.org/doc/faq#nil_error for an in depth explanation

The above example shows how to implement a simple custom Error type. But what if we need more sophisticated error handling? In this case, we have to refer to the net package approach:

package net

type Error interface {
    error
    Timeout() bool   // Is the error a timeout?
    Temporary() bool // Is the error temporary?
}

Using type assertion, we can check whether or not our error is of type net.Error, as shown in the following example. This allows us to refine our error handling -if a temporary error occurs on the network, it will sleep for 1 second, then retry the operation.

if nerr, ok := err.(net.Error); ok && nerr.Temporary() {
    time.Sleep(1e9)
    continue
}
if err != nil {
    log.Fatal(err)
}

Error handling

Go handles errors and checks the return values of functions in a C-like fashion, which is different to how most of the other major languages do. This makes the code more explicit and predictable, but also more verbose. To reduce the redundancy of our error-handling code, we can use abstract error handling functions that allow us to implement similar error handling behaviour:

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)
    }
}

The above example demonstrate how the data access and template call has detected an error. When an error occurs , a call to unified handler http.Error, returns a 500 error code to the client , and displays the corresponding error data. But when more and more HandleFunc calls are made, so error-handling logic code will increase. We can customize the router to reduce code (refer to the third chapter of HTTP for more detail).

type appHandler func(http.ResponseWriter, *http.Request) error

func (fn appHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    if err := fn(w, r); err != nil {
        http.Error(w, err.Error(), 500)
    }
}

Above we've defined a custom router. We can then register our handler as usual:

func init() {
    http.Handle("/view", appHandler(viewRecord))
}

The /view handler can then be handled by the following code; it is a lot simpler than our original implementation isn't it?

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)
}

The error handler example above will return the 500 Internal Error code to users when any errors occur, in addition to printing out the corresponding error code. In fact, we can customize the type of error returned to output a more developer friendly error message with information that is useful for debugging like so:

type appError struct {
    Error   error
    Message string
    Code    int
}

Our custom router can be changed accordingly:

type appHandler func(http.ResponseWriter, *http.Request) *appError

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)
    }
}

After we've finished modifying our custom error, our logic can be changed as follows:

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
}

As shown above, we can return different error codes and error messages in our views, depending on the situation. Although this version of our code functions similarly to the previous version, it's more explicit, and its error message prompts are more comprehensible. All of these factors can help to make your application more scalable as complexity increases.

Summary

Fault tolerance is a very important aspect of any programming language. In Go, it is achieved through error handling. Although Error is only one interface, it can have many variations in the way that it's implemented, and we can customize it according to our needs on a case by case basis. By introducing these various error handling concepts, we hope that you will have gained some insight on how to implement better error handling schemes in your own web applications.

results matching ""

    No results matching ""