Error handling
Error handling
Go does not have an exception mechanism, like the try/catch in Java or .NET for instance: you cannot throw exceptions. Instead it has a defer-panic-and-recover mechanism.
The Go way to handle errors is for functions and methods to return an error object as their only or last return value—or nil if no error occurred—and for calling functions to always check the error they receive.
Handle the errors and return from the function in which the error occurred with an error message to the user: that way if something does go wrong, your program will continue to function and the user will be notified. The purpose of panic-and-recover is to deal with genuinely exceptional (so unexpected) problems and not with normal errors.
The idiomatic way in Go to detect and report error-conditions
- A function which can result in an error returns two variables, a value and an error-code; the latter is nil in case of success, and != nil in case of an error-condition.
- After the function call the error is checked, in case of an error ( if error != nil) the execution of the actual function (or if necessary the entire program) is stopped.
!!Never ignore errors, because ignoring them can lead to program crashes!!
Error interface
Go has a predefined error interface type:
type error interface { Error() string }
Defining errors
err := errors.New("math - square root of negative number")
Making an error-object with fmt
if f < 0 { return 0, fmt.Errorf("math: square root of negative number %g", f) }
Run-time exceptions & panicking
When execution errors occur, such as attempting to index an array out of bounds or a type assertion failing, the Go runtime triggers a run-time panic with a value of the interface type runtime.Error, and the program crashes with a message of the error; this value has a RuntimeError()-method, to distinguish it from a normal error.
A panic can also be initiated from code with the panic function is used, which effectively creates a run-time error that will stop the program. It takes 1 argument of any type, usually a string, to be printed out when the program dies. The Go runtime takes care to stop the program and issuing some debug information.
If panic is called from a nested function, it immediately stops execution of the current function, all defer statements are guaranteed to execute and then control is given to the function caller, which receives this call to panic. This bubbles up to the top level, executing defers, and at the top of the stack the program crashes and the error condition is reported on the command-line using the value given to panic: this termination sequence is called panicking.
Error-handling & panicking in custom package
Best practice for custom package
a. Always recover from panic in your package: no explicit panic() should be allowed to cross a package boundary
b. Return errors as error values to the callers of your package.
Sample
Parse package
package parse import ( "fmt" "strings" "strconv" ) // A ParseError indicates an error in converting a word into an integer. type ParseError struct { Index int // The index into the space-separated list of words. Word string // The word that generated the parse error. // The raw error that precipitated this error, if any. Error err } // String returns a human-readable error message. func (e *ParseError) String() string { return fmt.Sprintf("pkg parse: error parsing %q as int", e.Word) } // Parse parses the space-separated words in in put as integers. func Parse(input string) (numbers []int, err error) { defer func() { if r := recover(); r != nil { var ok bool err, ok = r.(error) if !ok { err = fmt.Errorf("pkg: %v", r) } } }() fields := strings.Fields(input) numbers = fields2numbers(fields) // here panic can occur return } func fields2numbers(fields []string) (numbers []int) { if len(fields) == 0 { panic("no words to parse") } for idx, field := range fields { num, err := strconv.Atoi(field) if err != nil { panic(&ParseError{idx, field, err}) } numbers = append(numbers, num) } return }
main package
func main() { var examples = []string{ "1 2 3 4 5", "100 50 25 12.5 6.25", "2 + 2 = 4", "1st class", "" } for _, ex := range examples { fmt.Printf("Parsing %q:\n ", ex) nums, err := parse.Parse(ex) if err != nil { // here String() method from ParseError is used fmt.Println(err) continue } fmt.Println(nums) } /* Output: Parsing "w1 2 3 4 5": 360 Ivo Balbaert [1 2 3 4 5] Parsing "100 50 25 12.5 6.25": pkg parse: error parsing "12.5" as int Parsing "2 + 2 = 4": pkg parse: error parsing "+" as int Parsing "1st class": pkg parse: error parsing "1st" as int Parsing "": pkg: no words to parse */
Recover
recover is only useful when called inside a deferred function (see § 6.4) : it then retrieves the error value passed through the call of panic; when used in normal execution a call to recover will return nil and have no other effect.
Summarized: panic causes the stack to unwind until a deferred recover() is found or the program terminates
Similar try-catch block in Go
func protect(g func()) {
defer func() {
log.Println("done")
// Println executes normally even if there is a panic
if err := recover(); err != nil {
log.Printf("run time panic: %v", err)
}
}()
log.Println("start")
g() // possible runtime-error
}
Sample of panic, defer & recover
func badCall() {
panic("bad end")
}
func test() {
defer func() {
if e := recover(); e != nil {
fmt.Printf("Panicking %s\r\n", e)
}
}()
badCall()
fmt.Printf("After bad call\r\n")
}
func main() {
fmt.Printf("Calling test\r\n")
test()
fmt.Printf("Test completed\r\n")
}
An error-handling scheme with closures
Combining the defer/panic/recover mechanism with closures can result in a far more elegant scheme that we will now discuss. However it is only applicable when all functions have the same signature, which is rather restrictive.
The scheme uses 2 helper functions:
i) check: a function which tests whether an error occurred, and panics if so:
func check(err error) { if err != nil { panic(err) } }
ii) errorhandler: this is a wrapper function. It takes a function fn of our type fType1 and returns such a function by calling fn. However it contains the defer/recover mechanism
func errorHandler(fn fType1) fType1 { return func(a type1, b type2) { defer func() { if e, ok := recover().(error); ok { log.Printf("run time panic: %v", err) } }() fn(a, b) } }
Start external program
func main() {
// 1) os.StartProcess //
/*********************/
/* Linux: */
env := os.Environ()
procAttr := &os.ProcAttr{
Env: env,
Files: []*os.File{
os.Stdin,
os.Stdout,
os.Stderr,
},
}
pid, err := os.StartProcess("/bin/ls", []string{"ls", "-l"}, procAttr)
if err != nil {
fmt.Printf("Error %v starting process!", err) //
os.Exit(1)
}
fmt.Printf("The process id is %v", pid)
/* Output:
The process id is &{21275 0 0 {{0 0} 0 0 0 0}}The process id is &{21276 0 0 {{0 0} 0 0 0 0}}total 54
-rwxrwxrwx 1 root root 250 Sep 21 19:33 csv_data.txt
-rwxrwxrwx 1 root root 25227 Oct 4 23:34 hello.go
-rwxrwxrwx 1 root root 6708 Sep 21 10:25 hello.go.txt
-rwxrwxrwx 1 root root 130 Sep 21 11:08 output.txt
-rwxrwxrwx 1 root root 8898 Sep 21 12:10 target_hello.txt
-rwxrwxrwx 1 root root 1619 Sep 22 14:40 urlshorten.go.txt
-rwxrwxrwx 1 root root 182 Sep 21 13:50 vcard.json
*/
// 2nd example: show all processes
pid, err = os.StartProcess("/bin/ps", []string{"-e", "opid,ppid,comm"}, procAttr)
if err != nil {
fmt.Printf("Error %v starting process!", err) //
os.Exit(1)
}
fmt.Printf("The process id is %v", pid)
// 2) cmd.Run //
/***************/
cmd := exec.Command("gedit") // this opens a gedit-window
err = cmd.Run()
if err != nil {
fmt.Printf("Error %v executing command!", err)
os.Exit(1)
}
fmt.Printf("The command is %v", cmd)
}
Testing
Table-driven test
var tests = [] struct { in // Test table string out string }{ {"in1", "exp1"}, {"in2", "exp2"}, {"in3", "exp3"}, // .... } func verify(t *testing.T, testnum int, testcase, input, output, expected string) { if input != output { t.Errorf("%d. %s with input = %s: output %s != %s", testnum, testcase, input, output, expected) } } func TestFunction(t *testing.T) { for i, tt := range tests { s := FuncToBeTested(tt.in) verify(t, i, "FuncToBeTested: ", tt.in, s, tt.out) } }