Error Handling
In Go, there is a built-in error
type. The different values of error
type
indicate an abnormal state. Usually in Go, if the error
value is not nil
then an error has occurred. It must be dealt with in order to allow the
application to recover from that state without crashing.
A simple example taken from the Go blog follows:
if err != nil {
// handle the error
}
Not only can the built-in errors be used, we can also specify our own error
types. This can be achieved by using the errors.New
function.
Example:
{...}
if f < 0 {
return 0, errors.New("math: square root of negative number")
}
//If an error has occurred print it
if err != nil {
fmt.Println(err)
}
{...}
If we need to format the string containing the invalid argument to see what
caused the error, the Errorf
function in the fmt
package allows us to do
this.
{...}
if f < 0 {
return 0, fmt.Errorf("math: square root of negative number %g", f)
}
{...}
When dealing with error logs, developers should ensure no sensitive information is disclosed in the error responses, as well as guarantee that no error handlers leak information (e.g. debugging, or stack trace information).
In Go, there are additional error handling functions, these functions are
panic
, recover
and defer
. When an application state is panic
its
normal execution is interrupted, any defer
statements are executed, and
then the function returns to its caller. recover
is usually used inside
defer
statements and allows the application to regain control over a
panicking routine, and return to normal execution.
The following snippet, based on the Go documentation explains the execution
flow:
func main () {
start()
fmt.Println("Returned normally from start().")
}
func start () {
defer func () {
if r := recover(); r != nil {
fmt.Println("Recovered in start()")
}
}()
fmt.Println("Called start()")
part2(0)
fmt.Println("Returned normally from part2().")
}
func part2 (i int) {
if i > 0 {
fmt.Println("Panicking in part2()!")
panic(fmt.Sprintf("%v", i))
}
defer fmt.Println("Defer in part2()")
fmt.Println("Executing part2()")
part2(i + 1)
}
Output:
Called start()
Executing part2()
Panicking in part2()!
Defer in part2()
Recovered in start()
Returned normally from start().
By examining the output, we can see how Go can handle panic
situations and
recover from them, allowing the application to resume its normal state. These
functions allow for a graceful recovery from an otherwise unrecoverable
failure.
It is worth noting that defer
usages also include Mutex Unlocking, or
loading content after the surrounding function has executed (e.g. footer).
In the log
package there is also a log.Fatal
. Fatal level is effectively
logging the message, then calling os.Exit(1)
.
Which means:
- Defer statements will not be executed.
- Buffers will not be flushed.
- Temporary files and directories are not removed.
Considering all the previously mentioned points, we can see how log.Fatal
differs from Panic
and why it should be used carefully.
Some examples of the possible usage of log.Fatal
are:
- Set up logging and check whether we have a healthy environment and parameters. If we don't, then there's no need to execute our main().
- An error that should never occur and that we know that it's unrecoverable.
- If a non-interactive process encounters an error and cannot complete, there is no way to notify the user about this error. It's best to stop the execution before additional problems can emerge from this failure.
To demonstrate, here's an example of an initialization failure:
func initialize(i int) {
...
//This is just to deliberately crash the function.
if i < 2 {
fmt.Printf("Var %d - initialized\n", i)
} else {
//This was never supposed to happen, so we'll terminate our program.
log.Fatal("Init failure - Terminating.")
}
}
func main() {
i := 1
for i < 3 {
initialize(i)
i++
}
fmt.Println("Initialized all variables successfully")
}
It's important to assure that in case of an error associated with the security controls, its access is denied by default.