Error Handling In GoLang
Go’s error-handling features, which are very different from exception-based systems in other languages, are essential to creating software that is dependable and strong. Go encourages developers to check and handle problems openly by highlighting the fact that they are values that functions return like any other value.
Built-in error type and Basic Error Creation
Error() is the only method in the built-in error type of Go, which is an interface that returns a string:
type error interface {
Error() string
}
By merely using this technique, every type can be turned into an error with this design.
New error values can be generated by using:
errors.New
: This function, which comes from the errors standard package, returns a new error after receiving a string.- The fmt package automatically retrieves the string representation by calling the err variable’s Error() function.
fmt.Errorf
: This function lets you write error messages that are dynamically formatted, like fmt.Printf.- By using changeable data, this enables more informative error messages.
Handling Errors
To deal with problems in Go, the most popular and idiomatic method is to see if the returned error value is not zero. There was no error, as shown by a nil value.
In addition to the result, functions frequently return a possible error. The last return value is typically the 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 // Returns nil for the error if successful
}
func main() {
// Call function and capture both return values
name, err := capitalize("govindhtech")
if err != nil { // Check if an error occurred
fmt.Println("Could not capitalize:", err)
return // Exit if error
}
fmt.Println("Capitalized name:", name)
}
Output
Capitalized name: GOVINDHTECH
If the error is all that matters to you, you can reject other return values by using the blank identifier (_
):
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("") // Discard the string value
if err != nil {
fmt.Println("Could not capitalize:", err)
return
}
fmt.Println("Success!")
}
Output
Could not capitalize: no name provided
In order to streamline function calls and error handling, Go additionally provides an optional assignment clause in if statements:
package main
import (
"errors"
"fmt"
)
func boom() error {
return errors.New("barnacles")
}
func main() {
if err := boom(); err != nil { // Assigns and checks in one line
fmt.Println("An error occurred:", err)
return
}
fmt.Println("Anchors away!")
}
Output
An error occurred: barnacles
Error handling (often known as the “sad path”) is kept at the second indent level, while the “happy path” logic remains at the first.
Custom Error Types and Type Assertions
It is possible to define your own struct that implements the error interface for more intricate error information. You can now attach more fields than just a string message with this.
Example of a custom error type:
package main
import (
"fmt"
"errors"
"os"
)
type RequestError struct {
StatusCode int
Err error
}
func (r *RequestError) Error() string {
return fmt.Sprintf("status %d: err %v", r.StatusCode, r.Err)
}
func doRequest() error {
return &RequestError{
StatusCode: 503,
Err: errors.New("unavailable"),
}
}
func main() {
err := doRequest()
if err != nil {
fmt.Println(err)
os.Exit(1)
}
fmt.Println("success!")
}
Output
status 503: err unavailable
RequestError in this case contains both an underlying issue and a status code. Error() converts these details into a single string when it is called.
A type assertion is used to access the particular properties of a custom error type, as the error interface only exposes Error():
package main
import (
"errors"
"fmt"
"net/http"
"os"
)
type RequestError struct {
StatusCode int
Err error
}
func (r *RequestError) Error() string {
return r.Err.Error()
}
func (r *RequestError) Temporary() bool {
return r.StatusCode == http.StatusServiceUnavailable // 503
}
func doRequest() error {
return &RequestError{
StatusCode: 503,
Err: errors.New("unavailable"),
}
}
func main() {
err := doRequest()
if err != nil {
fmt.Println(err)
// Attempt a type assertion to the concrete RequestError type
if re, ok := err.(*RequestError); ok {
if re.Temporary() {
fmt.Println("This request can be tried again")
} else {
fmt.Println("This request cannot be tried again")
}
}
os.Exit(1)
}
}
Output
unavailable
This request can be tried again
exit status 1
Converting the err
interface to a (*RequestError)
is the goal of the re, ok := err.(*RequestError)
syntax. If the conversion is successful, the Temporary()
method will be accessible as ok
will be true
.
Wrapping Errors: In order to give debugging context, errors can also be wrapped. One mistake sets the stage for another, resulting in a series of mistakes.
package main
import (
"errors"
"fmt"
)
type WrappedError struct {
Context string
Err error
}
func (w *WrappedError) Error() string {
return fmt.Sprintf("%s: %v", w.Context, w.Err)
}
func Wrap(err error, info string) *WrappedError {
return &WrappedError{
Context: info,
Err: err,
}
}
func main() {
err := errors.New("boom!")
err = Wrap(err, "main")
fmt.Println(err)
}
Output
main: boom!
This lets you give an error context as it moves through your application’s levels.
defer
statement
After the surrounding code has finished running, the defer statement plans to execute a function call. Because it maintains the cleanup code close to where the resource was accessed, it is especially helpful for resource cleanup (such as shutting down files or network connections) and guarantees that it continues to run even in the event of a panic.
package main
import (
"io"
"log"
"os"
)
func main() {
if err := write("readme.txt", "This is a readme file"); err != nil {
log.Fatal("failed to write file:", err)
}
}
func write(fileName string, text string) error {
file, err := os.Create(fileName)
if err != nil {
return err
}
// Defer the file.Close() call. It will run when 'write' function returns.
defer file.Close()
_, err = io.WriteString(file, text)
if err != nil {
return err // file.Close() will still be called here due to defer
}
// Optionally, return file.Close() here to report its error specifically.
// The deferred file.Close() will still run afterwards but its error will be ignored.
return file.Close()
}
Output
This is a readme file
In this instance, defer the file.It is ensured via Close().Whether io is used to invoke Close().Whether WriteString works or not, resource leaks are avoided.
panic
and recover
Although Go promotes the use of explicit error handling, it also offers panic and recovery for unforeseen or unusual situations.
panic
: Immediately stopping the current function’s execution, this built-in function starts “panicking.” It performs any deferred operations as it unwinds the call stack. Program termination occurs if no function in the call stack recovers. Generally speaking, panics signify a programming fault (such as accessing an out-of-bounds array index or nil pointer dereference) or a situation from which there is no simple solution.
recover
: The value supplied to panic is returned by this built-in method, which also ends a panicking goroutine. It only works when invoked within a deferred function.
In this instance, panic is triggered while dividing by zero. Nevertheless, the deferred function logs the panic, uses recover() to catch it, and permits the application to continue running rather than crashing. Recovery will return nil in this scenario, making panic indistinguishable from a non-panicked state, hence it is imperative to avoid passing nil to panic.
In conclusion, Go’s error handling philosophy emphasizes explicit checks and unambiguous notification of predicted failures through error values. Defer is used for cleanup, while panic/recover is used for circumstances that are truly unexpected and unrecoverable.