One place for hosting & domains

      Handling

      Handling Panics in Go


      Introduction

      Errors that a program encounters fall into two broad categories: those the programmer has anticipated and those the programmer has not. The error interface that we have covered in our previous two articles on error handling largely deal with errors that we expect as we are writing Go programs. The error interface even allows us to acknowledge the rare possibility of an error occurring from function calls, so we can respond appropriately in those situations.

      Panics fall into the second category of errors, which are unanticipated by the programmer. These unforeseen errors lead a program to spontaneously terminate and exit the running Go program. Common mistakes are often responsible for creating panics. In this tutorial, we’ll examine a few ways that common operations can produce panics in Go, and we’ll also see ways to avoid those panics. We’ll also use defer statements along with the recover function to capture panics before they have a chance to unexpectedly terminate our running Go programs.

      Understanding Panics

      There are certain operations in Go that automatically return panics and stop the program. Common operations include indexing an array beyond its capacity, performing type assertions, calling methods on nil pointers, incorrectly using mutexes, and attempting to work with closed channels. Most of these situations result from mistakes made while programming that the compiler has no ability to detect while compiling your program.

      Since panics include detail that is useful for resolving an issue, developers commonly use panics as an indication that they have made a mistake during a program’s development.

      Out of Bounds Panics

      When you attempt to access an index beyond the length of a slice or the capacity of an array, the Go runtime will generate a panic.

      The following example makes the common mistake of attempting to access the last element of a slice using the length of the slice returned by the len builtin. Try running this code to see why this might produce a panic:

      package main
      
      import (
          "fmt"
      )
      
      func main() {
          names := []string{
              "lobster",
              "sea urchin",
              "sea cucumber",
          }
          fmt.Println("My favorite sea creature is:", names[len(names)])
      }
      

      This will have the following output:

      Output

      panic: runtime error: index out of range [3] with length 3 goroutine 1 [running]: main.main() /tmp/sandbox879828148/prog.go:13 +0x20

      The name of the panic’s output provides a hint: panic: runtime error: index out of range. We created a slice with three sea creatures. We then tried to get the last element of the slice by indexing that slice with the length of the slice using the len builtin function. Remember that slices and arrays are zero-based; so the first element is zero and the last element in this slice is at index 2. Since we try to access the slice at the third index, 3, there is no element in the slice to return because it is beyond the bounds of the slice. The runtime has no option but to terminate and exit since we have asked it to do something impossible. Go also can’t prove during compilation that this code will try to do this, so the compiler cannot catch this.

      Notice also that the subsequent code did not run. This is because a panic is an event that completely halts the execution of your Go program. The message produced contains multiple pieces of information helpful for diagnosing the cause of the panic.

      Anatomy of a Panic

      Panics are composed of a message indicating the cause of the panic and a stack trace that helps you locate where in your code the panic was produced.

      The first part of any panic is the message. It will always begin with the string panic:, which will be followed with a string that varies depending on the cause of the panic. The panic from the previous exercise has the message:

      panic: runtime error: index out of range [3] with length 3
      

      The string runtime error: following the panic: prefix tells us that the panic was generated by the language runtime. This panic is telling us that we attempted to use an index [3] that was out of range of the slice’s length 3.

      Following this message is the stack trace. Stack traces form a map that we can follow to locate exactly what line of code was executing when the panic was generated, and how that code was invoked by earlier code.

      goroutine 1 [running]:
      main.main()
          /tmp/sandbox879828148/prog.go:13 +0x20
      

      This stack trace, from the previous example, shows that our program generated the panic from the file /tmp/sandbox879828148/prog.go at line number 13. It also tells us that this panic was generated in the main() function from the main package.

      The stack trace is broken into separate blocks—one for each goroutine in your program. Every Go program’s execution is accomplished by one or more goroutines that can each independently and simultaneously execute parts of your Go code. Each block begins with the header goroutine X [state]:. The header gives the ID number of the goroutine along with the state that it was in when the panic occurred. After the header, the stack trace shows the function that the program was executing when the panic happened, along with the filename and line number where the function executed.

      The panic in the previous example was generated by an out-of-bounds access to a slice. Panics can also be generated when methods are called on pointers that are unset.

      Nil Receivers

      The Go programming language has pointers to refer to a specific instance of some type existing in the computer’s memory at runtime. Pointers can assume the value nil indicating that they are not pointing at anything. When we attempt to call methods on a pointer that is nil, the Go runtime will generate a panic. Similarly, variables that are interface types will also produce panics when methods are called on them. To see the panics generated in these cases, try the following example:

      package main
      
      import (
          "fmt"
      )
      
      type Shark struct {
          Name string
      }
      
      func (s *Shark) SayHello() {
          fmt.Println("Hi! My name is", s.Name)
      }
      
      func main() {
          s := &Shark{"Sammy"}
          s = nil
          s.SayHello()
      }
      

      The panics produced will look like this:

      Output

      panic: runtime error: invalid memory address or nil pointer dereference [signal SIGSEGV: segmentation violation code=0xffffffff addr=0x0 pc=0xdfeba] goroutine 1 [running]: main.(*Shark).SayHello(...) /tmp/sandbox160713813/prog.go:12 main.main() /tmp/sandbox160713813/prog.go:18 +0x1a

      In this example, we defined a struct called Shark. Shark has one method defined on its pointer receiver called SayHello that will print a greeting to standard out when called. Within the body of our main function, we create a new instance of this Shark struct and request a pointer to it using the & operator. This pointer is assigned to the s variable. We then reassign the s variable to the value nil with the statement s = nil. Finally we attempt to call the SayHello method on the variable s. Instead of receiving a friendly message from Sammy, we receive a panic that we have attempted to access an invalid memory address—this is because we set the s variable to nil.

      While we have set s to nil explicitly in this example, in practice this happens less obviously. When you see panics involving nil pointer dereference, be sure that you have properly assigned any pointer variables that you may have created.

      Panics generated from nil pointers and out-of-bounds accesses are two commonly occurring panics generated by the runtime. It is also possible to manually generate a panic using a builtin function.

      Using the panic Builtin Function

      We can also generate panics of our own using the panic built-in function. It takes a single string as an argument, which is the message the panic will produce. Typically this message is less verbose than rewriting our code to return an error. Furthermore, we can use this within our own packages to indicate to developers that they may have made a mistake when using our package’s code. Whenever possible, best practice is to try to return error values to consumers of our package.

      Run this code to see a panic generated from a function called from another function:

      package main
      
      func main() {
          foo()
      }
      
      func foo() {
          panic("oh no!")
      }
      

      The panic output produced looks like:

      Output

      panic: oh no! goroutine 1 [running]: main.foo(...) /tmp/sandbox494710869/prog.go:8 main.main() /tmp/sandbox494710869/prog.go:4 +0x40

      Here we define a function foo that calls the panic builtin with the string "oh no!". This function is called by our main function. Notice how the output has the message panic: oh no! and the stack trace shows a single goroutine with two lines in the stack trace: one for the main() function and one for our foo() function.

      We’ve seen that panics appear to terminate our program where they are generated. This can create problems when there are open resources that need to be properly closed. Go provides a mechanism to execute some code always, even in the presence of a panic.

      Deferred Functions

      Your program may have resources that it must clean up properly, even while a panic is being processed by the runtime. Go allows you to defer the execution of a function call until its calling function has completed execution. Deferred functions run even in the presence of a panic, and are used as a safety mechanism to guard against the chaotic nature of panics. Functions are deferred by calling them as usual, then prefixing the entire statement with the defer keyword, as in defer sayHello(). Run this example to see how a message can be printed even though a panic was produced:

      package main
      
      import "fmt"
      
      func main() {
          defer func() {
              fmt.Println("hello from the deferred function!")
          }()
      
          panic("oh no!")
      }
      

      The output produced from this example will look like:

      Output

      hello from the deferred function! panic: oh no! goroutine 1 [running]: main.main() /Users/gopherguides/learn/src/github.com/gopherguides/learn//handle-panics/src/main.go:10 +0x55

      Within the main function of this example, we first defer a call to an anonymous function that prints the message "hello from the deferred function!". The main function then immediately produces a panic using the panic function. In the output from this program, we first see that the deferred function is executed and prints its message. Following this is the panic we generated in main.

      Deferred functions provide protection against the surprising nature of panics. Within deferred functions, Go also provides us the opportunity to stop a panic from terminating our Go program using another built-in function.

      Handling Panics

      Panics have a single recovery mechanism—the recover builtin function. This function allows you to intercept a panic on its way up through the call stack and prevent it from unexpectedly terminating your program. It has strict rules for its use, but can be invaluable in a production application.

      Since it is part of the builtin package, recover can be called without importing any additional packages:

      package main
      
      import (
          "fmt"
          "log"
      )
      
      func main() {
          divideByZero()
          fmt.Println("we survived dividing by zero!")
      
      }
      
      func divideByZero() {
          defer func() {
              if err := recover(); err != nil {
                  log.Println("panic occurred:", err)
              }
          }()
          fmt.Println(divide(1, 0))
      }
      
      func divide(a, b int) int {
          return a / b
      }
      

      This example will output:

      Output

      2009/11/10 23:00:00 panic occurred: runtime error: integer divide by zero we survived dividing by zero!

      Our main function in this example calls a function we define, divideByZero. Within this function, we defer a call to an anonymous function responsible for dealing with any panics that may arise while executing divideByZero. Within this deferred anonymous function, we call the recover builtin function and assign the error it returns to a variable. If divideByZero is panicking, this error value will be set, otherwise it will be nil. By comparing the err variable against nil, we can detect if a panic occurred, and in this case we log the panic using the log.Println function, as though it were any other error.

      Following this deferred anonymous function, we call another function that we defined, divide, and attempt to print its results using fmt.Println. The arguments provided will cause divide to perform a division by zero, which will produce a panic.

      In the output to this example, we first see the log message from the anonymous function that recovers the panic, followed by the message we survived dividing by zero!. We have indeed done this, thanks to the recover builtin function stopping an otherwise catastrophic panic that would terminate our Go program.

      The err value returned from recover() is exactly the value that was provided to the call to panic(). It’s therefore critical to ensure that the err value is only nil when a panic has not occurred.

      Detecting Panics with recover

      The recover function relies on the value of the error to make determinations as to whether a panic occurred or not. Since the argument to the panic function is an empty interface, it can be any type. The zero value for any interface type, including the empty interface, is nil. Care must be taken to avoid nil as an argument to panic as demonstrated by this example:

      package main
      
      import (
          "fmt"
          "log"
      )
      
      func main() {
          divideByZero()
          fmt.Println("we survived dividing by zero!")
      
      }
      
      func divideByZero() {
          defer func() {
              if err := recover(); err != nil {
                  log.Println("panic occurred:", err)
              }
          }()
          fmt.Println(divide(1, 0))
      }
      
      func divide(a, b int) int {
          if b == 0 {
              panic(nil)
          }
          return a / b
      }
      
      

      This will output:

      Output

      we survived dividing by zero!

      This example is the same as the previous example involving recover with some slight modifications. The divide function has been altered to check if its divisor, b, is equal to 0. If it is, it will generate a panic using the panic builtin with an argument of nil. The output, this time, does not include the log message showing that a panic occurred even though one was created by divide. This silent behavior is why it is very important to ensure that the argument to the panic builtin function is not nil.

      Conclusion

      We have seen a number of ways that panics can be created in Go and how they can be recovered from using the recover builtin. While you may not necessarily use panic yourself, proper recovery from panics is an important step of making Go applications production-ready.

      You can also explore our entire How To Code in Go series.



      Source link

      Handling Errors in Go


      Robust code needs to react correctly to unexpected circumstances like bad user input, faulty network connections, and failing disks. Error handling is the process of identifying when your program is in an unexpected state, and taking steps to record diagnostic information for later debugging.

      Unlike other languages that require developers to handle errors with specialized syntax, errors in Go are values with the type error returned from functions like any other value. To handle errors in Go, we must examine these errors that functions could return, decide if an error has occurred, and take proper action to protect data and tell users or operators that the error occurred.

      Creating Errors

      Before we can handle errors, we need to create some first. The standard library provides two built-in functions to create errors: errors.New and fmt.Errorf. Both of these functions allow you to specify a custom error message that you can later present to your users.

      errors.New takes a single argument—an error message as a string that you can customize to alert your users what went wrong.

      Try running the following example to see an error created by errors.New printed to standard output:

      package main
      
      import (
          "errors"
          "fmt"
      )
      
      func main() {
          err := errors.New("barnacles")
          fmt.Println("Sammy says:", err)
      }
      

      Output

      Sammy says: barnacles

      We used the errors.New function from the standard library to create a new error message with the string "barnacles" as the error message. We’ve followed convention here by using lowercase for the error message as the Go Programming Language Style Guide suggests.

      Finally, we used the fmt.Println function to combine our error message with "Sammy says:".

      The fmt.Errorf function allows you to dynamically build an error message. Its first argument is a string containing your error message with placeholder values such as %s for a string and %d for an integer. fmt.Errorf interpolates the arguments that follow this formatting string into those placeholders in order:

      package main
      
      import (
          "fmt"
          "time"
      )
      
      func main() {
          err := fmt.Errorf("error occurred at: %v", time.Now())
          fmt.Println("An error happened:", err)
      }
      

      Output

      An error happened: Error occurred at: 2019-07-11 16:52:42.532621 -0400 EDT m=+0.000137103

      We used the fmt.Errorf function to build an error message that would include the current time. The formatting string we provided to fmt.Errorf contains the %v formatting directive that tells fmt.Errorf to use the default formatting for the first argument provided after the formatting string. That argument will be the current time, provided by the time.Now function from the standard library. Similarly to the earlier example, we combine our error message with a short prefix and print the result to standard output using the fmt.Println function.

      Handling Errors

      Typically you wouldn’t see an error created like this to be used immediately for no other purpose, as in the previous example. In practice, it’s far more common to create an error and return it from a function when something goes wrong. Callers of that function will then use an if statement to see if the error was present or nil—an uninitialized value.

      This next example includes a function that always returns an error. Notice when you run the program that it produces the same output as the previous example even though a function is returning the error this time. Declaring an error in a different location does not change the error’s message.

      package main
      
      import (
          "errors"
          "fmt"
      )
      
      func boom() error {
          return errors.New("barnacles")
      }
      
      func main() {
          err := boom()
      
          if err != nil {
              fmt.Println("An error occurred:", err)
              return
          }
          fmt.Println("Anchors away!")
      }
      

      Output

      An error occurred: barnacles

      Here we define a function called boom() that returns a single error that we construct using errors.New. We then call this function and capture the error with the line err := boom().
      Once we assign this error, we check to see if it was present with the if err != nil conditional. Here the conditional will always evaluate to true, since we are always returning an error from boom().

      This won’t always be the case, so it’s good practice to have logic handling cases where an error is not present (nil) and cases where the error is present. When the error is present, we use fmt.Println to print our error along with a prefix as we have done in earlier examples. Finally, we use a return statement to skip the execution of fmt.Println("Anchors away!"), since that should only execute when no error occurred.

      Note: The if err != nil construction shown in the last example is the workhorse of error handling in the Go programming language. Wherever a function could produce an error, it’s important to use an if statement to check whether one occurred. In this way, idiomatic Go code naturally has its “happy path” logic at the first indent level, and all the “sad path” logic at the second indent level.

      If statements have an optional assignment clause that can be used to help condense calling a function and handling its errors.

      Run the next program to see the same output as our earlier example, but this time using a compound if statement to reduce some boilerplate:

      package main
      
      import (
          "errors"
          "fmt"
      )
      
      func boom() error {
          return errors.New("barnacles")
      }
      
      func main() {
          if err := boom(); err != nil {
              fmt.Println("An error occurred:", err)
              return
          }
          fmt.Println("Anchors away!")
      }
      

      Output

      An error occurred: barnacles

      As before, we have a function, boom(), that always returns an error. We assign the error returned from boom() to err as the first part of the if statement. In the second part of the if statement, following the semicolon, that err variable is then available. We check to see if the error was present and print our error with a short prefix string as we’ve done previously.

      In this section, we learned how to handle functions that only return an error. These functions are common, but it’s also important to be able to handle errors from functions that can return multiple values.

      Returning Errors Alongside Values

      Functions that return a single error value are often those that effect some stateful change, like inserting rows to a database. It’s also common to write functions that return a value if they completed successfully along with a potential error if that function failed. Go permits functions to return more than one result, which can be used to simultaneously return a value and an error type.

      To create a function that returns more than one value, we list the types of each returned value inside parentheses in the signature for the function. For example, a capitalize function that returns a string and an error would be declared using func capitalize(name string) (string, error) {}. The (string, error) part tells the Go compiler that this function will return a string and an error, in that order.

      Run the following program to see the output from a function that returns both a string and an error:

      package main
      
      import (
          "errors"
          "fmt"
          "strings"
      )
      
      func capitalize(name string) (string, error) {
          if name == "" {
              return "", errors.New("no name provided")
          }
          return strings.ToTitle(name), nil
      }
      
      func main() {
          name, err := capitalize("sammy")
          if err != nil {
              fmt.Println("Could not capitalize:", err)
              return
          }
      
          fmt.Println("Capitalized name:", name)
      }
      

      Output

      Capitalized name: SAMMY

      We define capitalize() as a function that takes a string (the name to be capitalized) and returns a string and an error value. In main(), we call capitalize() and assign the two values returned from the function to the name and err variables by separating them with commas on the left-hand side of the := operator. After this, we perform our if err != nil check as in earlier examples, printing the error to standard output using fmt.Println if the error was present. If no error was present, we print Capitalized name: SAMMY.

      Try changing the string "sammy" in name, err := capitalize("sammy") to the empty string ("") and you’ll receive the error Could not capitalize: no name provided instead.

      The capitalize function will return an error when callers of the function provide an empty string for the name parameter. When the name parameter is not the empty string, capitalize() uses strings.ToTitle to capitalize the name parameter and returns nil for the error value.

      There are some subtle conventions that this example follows that is typical of Go code, yet not enforced by the Go compiler. When a function returns multiple values, including an error, convention requests that we return the error as the last item. When returning an error from a function with multiple return values, idiomatic Go code also will set each non-error value to a zero value. Zero values are, for example, an empty string for strings, 0 for integers, an empty struct for struct types, and nil for interface and pointer types, to name a few. We cover zero values in more detail in our tutorial on variables and constants.

      Reducing boilerplate

      Adhering to these conventions can become tedious in situations where there are many values to return from a function. We can use an anonymous function to help reduce the boilerplate. Anonymous functions are procedures assigned to variables. In contrast to the functions we have defined in earlier examples, they are only available within the functions where you declare them—this makes them perfect to act as short pieces of reusable helper logic.

      The following program modifies the last example to include the length of the name that we’re capitalizing. Since it has three values to return, handling errors could become cumbersome without an anonymous function to assist us:

      package main
      
      import (
          "errors"
          "fmt"
          "strings"
      )
      
      func capitalize(name string) (string, int, error) {
          handle := func(err error) (string, int, error) {
              return "", 0, err
          }
      
          if name == "" {
              return handle(errors.New("no name provided"))
          }
      
          return strings.ToTitle(name), len(name), nil
      }
      
      func main() {
          name, size, err := capitalize("sammy")
          if err != nil {
              fmt.Println("An error occurred:", err)
          }
      
          fmt.Printf("Capitalized name: %s, length: %d", name, size)
      }
      

      Output

      Capitalized name: SAMMY, length: 5

      Within main(), we now capture the three returned arguments from capitalize as name, size, and err, respectively. We then check to see if capitalize returned an error by checking if the err variable was not equal to nil. This is important to do before attempting to use any of the other values returned by capitalize, because the anonymous function, handle, could set those to zero values. Since no error occurred because we provided the string "sammy", we print out the capitalized name and its length.

      Once again, you can try changing "sammy" to the empty string ("") to see the error case printed (An error occurred: no name provided).

      Within capitalize, we define the handle variable as an anonymous function. It takes a single error and returns identical values in the same order as the return values of capitalize. handle sets those values to zero values and forwards the error passed as its argument as the final return value. Using this, we can then return any errors encountered in capitalize by using the return statement in front of the call to handle with the error as its parameter.

      Remember that capitalize must return three values all the time, since that’s how we defined the function. Sometimes we don’t want to deal with all the values that a function could return. Fortunately, we have some flexibility in how we can use these values on the assignment side.

      Handling Errors from Multi-Return Functions

      When a function returns many values, Go requires us to assign each to a variable. In the last example, we do this by providing names for the two values returned from the capitalize function. These names should be separated by commas and appear on the left-hand side of the := operator. The first value returned from capitalize will be assigned to the name variable, and the second value (the error) will be assigned to the variable err. Occasionally, we’re only interested in the error value. You can discard any unwanted values that functions return using the special _ variable name.

      In the following program, we’ve modified our first example involving the capitalize function to produce an error by passing in the empty string (""). Try running this program to see how we’re able to examine just the error by discarding the first returned value with the _ variable:

      package main
      
      import (
          "errors"
          "fmt"
          "strings"
      )
      
      func capitalize(name string) (string, error) {
          if name == "" {
              return "", errors.New("no name provided")
          }
          return strings.ToTitle(name), nil
      }
      
      func main() {
          _, err := capitalize("")
          if err != nil {
              fmt.Println("Could not capitalize:", err)
              return
          }
          fmt.Println("Success!")
      }
      

      Output

      Could not capitalize: no name provided

      Within the main() function this time, we assign the capitalized name (the string returned first) to the underscore variable (_). At the same time, we assign the error returned by capitalize to the err variable. We then check if the error was present in the if err != nil conditional. Since we have hard-coded an empty string as an argument to capitalize in the line _, err := capitalize(""), this conditional will always evaluate to true. This produces the output "Could not capitalize: no name provided" printed by the call to the fmt.Println function within the body of the if statement. The return after this will skip the fmt.Println("Success!").

      Conclusion

      We’ve seen many ways to create errors using the standard library and how to build functions that return errors in an idiomatic way. In this tutorial, we’ve managed to successfully create various errors using the standard library errors.New and fmt.Errorf functions. In future tutorials, we’ll look at how to create our own custom error types to convey richer information to users.



      Source link