Error signaling and handling

At this point, let us address how to idiomatically signal and handle errors when you make a function call. If you have worked with languages such as Python, Java, or C#, you may be familiar with interrupting the flow of your executing code by throwing an exception when an undesirable state arises.

As we will explore in this section, Go has a simplified approach to error signaling and error handling that puts the onus on the programmer to handle possible errors immediately after a called function returns. Go discourages the notion of interrupting an execution by indiscriminately short-circuiting the executing program with an exception in the hope that it will be properly handled further up the call stack. In Go, the traditional way of signaling errors is to return a value of type error when something goes wrong during the execution of your function. So let us take a closer look how this is done.

Signaling errors

To better understand what has been described in the previous paragraph, let us start with an example. The following source code implements an anagram program, as described in Column 2 from Jon Bentley's popular Programming Pearls book (second edition). The code reads a dictionary file (dict.txt) and groups all words with the same anagram. If the code does not quite make sense, please see golang.fyi/ch05/anagram1.go for an annotated explanation of how each part of the program works.

package main 
 
import ( 
   "bufio" 
   "bytes" 
   "fmt" 
   "os" 
   "errors" 
) 
 
// sorts letters in a word (i.e. "morning" -> "gimnnor") 
func sortRunes(str string) string { 
   runes := bytes.Runes([]byte(str)) 
   var temp rune 
   for i := 0; i < len(runes); i++ { 
         for j := i + 1; j < len(runes); j++ { 
               if runes[j] < runes[i] { 
                     temp = runes[i] 
                     runes[i], runes[j] = runes[j], temp 
               } 
 
         } 
   } 
   return string(runes) 
} 
 
// load loads content of file fname into memory as []string 
func load(fname string) ([]string, error) { 
   if fname == "" { 
         return nil, errors.New( 
               "Dictionary file name cannot be empty.")  
   } 
 
   file, err := os.Open(fname) 
   if err != nil { 
         return nil, err 
   } 
   defer file.Close() 
 
   var lines []string 
   scanner := bufio.NewScanner(file) 
   scanner.Split(bufio.ScanLines) 
   for scanner.Scan() { 
         lines = append(lines, scanner.Text()) 
   } 
   return lines, scanner.Err() 
} 
 
func main() { 
   words, err := load("dict.txt")       
   if err != nil { 
         fmt.Println("Unable to load file:", err) 
         os.Exit(1) 
   } 
 
      anagrams := make(map[string][]string) 
   for _, word := range words { 
         wordSig := sortRunes(word) 
         anagrams[wordSig] = append(anagrams[wordSig], word) 
   } 
 
   for k, v := range anagrams { 
         fmt.Println(k, "->", v) 
   } 
} 

golang.fyiy/ch05/anagram1.go

Again, if you want a more detail explanation of the previous program, take a look at the link supplied earlier. The focus here is on error signaling used in the previous program. As a convention, Go code uses the built-in type error to signal when an error occurred during execution of a function. Therefore, a function must return a value of type error to indicate to its caller that something went wrong. This is illustrated in the following snippet of the load function (extracted from the previous example):

func load(fname string) ([]string, error) { 
   if fname == "" { 
       return nil, errors.New( 
         "Dictionary file name cannot be empty.")  
   } 
 
   file, err := os.Open(fname) 
   if err != nil { 
         return nil, err 
   } 
   ... 
} 

Notice that the load function returns multiple result parameters. One is for the expected value, in this case []string, and the other is the error value. Idiomatic Go dictates that the programmer returns a non-nil value for result of type error to indicate that something abnormal occurred during the execution of the function. In the previous snippet, the load function signals an error occurrence to its callers in two possible instances:

  • when the expected filename (fname) is empty
  • when the call to os.Open() fails (for example, permission error, or otherwise)

In the first case, when a filename is not provided, the code returns an error using errors.New() to create a value of type error to exit the function. In the second case, the os.Open function returns a pointer representing the file and an error assigned to the file and err variables respectively. If err is not nil (meaning an error was generated), the execution of the load function is halted prematurely and the value of err is returned to be handled by the calling function further up the call stack.

Note

When returning an error for a function with multiple result parameters, it is customary to return the zero-value for the other (non-error type) parameters. In the example, a value of nil is returned for the result of type []string. While not necessary, it simplifies error handling and avoids any confusion for function callers.

Error handling

As described previously, signaling of an erroneous state is as simple as returning a non-nil value, of type error, during execution of a function. The caller may choose to handle the error or return it for further evaluation up the call stack as was done in the load function. This idiom forces errors to propagate upwards until they are handled at some point. The next snippet shows how the error generated by the load function is handled in the main function:

func main() { 
   words, err := load("dict.txt") 
   if err != nil { 
         fmt.Println("Unable to load file:", err) 
         os.Exit(1) 
   } 
   ... 
} 

Since the main function is the topmost caller in the call stack, it handles the error by terminating the entire program.

This is all there is to the mechanics of error handling in Go. The language forces the programmer to always test for an erroneous state on every function call that returns a value of the type error. The if…not…nil error handling idiom may seem excessive and verbose to some, especially if you are coming from a language with formal exception mechanisms. However, the gain here is that the program can construct a robust execution flow where programmers always know where errors may come from and handle them appropriately.

The error type

The error type is a built-in interface and, therefore must be implemented before it can be used. Fortunately, the Go standard library comes with implementations ready to be used. We have already used one of the implementation from the package, errors:

errors.New("Dictionary file name cannot be empty.")  

You can also create parameterized error values using the fmt.Errorf function as shown in the following snippet:

func load(fname string) ([]string, error) { 
   if fname == "" { 
         return nil, errors.New( 
             "Dictionary file name cannot be emtpy.") 
   } 
 
   file, err := os.Open(fname) 
   if err != nil { 
         return nil, fmt.Errorf( 
             "Unable to open file %s: %s", fname, err) 
   } 
   ... 
} 

golang.fyi/ch05/anagram2.go

It is also idiomatic to assign error values to high-level variables so they can be reused throughout a program as needed. The following snippet pulled from http://golang.org/src/os/error.go shows the declaration of reusable errors associated with OS file operations:

var ( 
   ErrInvalid    = errors.New("invalid argument") 
   ErrPermission = errors.New("permission denied") 
   ErrExist      = errors.New("file already exists") 
   ErrNotExist   = errors.New("file does not exist") 
) 

http://golang.org/src/os/error.go

You can also create your own implementation of the error interface to create custom errors. This topic is revisited in Chapter 7Methods, Interfaces, and Objects where the book discusses the notion of extending types.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.225.72.245