Error Handling
Error handling is a crucial part of software development, if the errors are not handled properly then it's a nightmare to resolve the issue with the software. Golang error handling is cleaner and more straightforward compared with other languages.
In Go, for error handling, it has a built-in library called 'errors'. The library provides several functions to deal with errors. Unlike many other languages, in Go, functions often return both values and errors even though there is no explicit error handling within the function.
Example:
func (d *Date) Format() (string, error) {
formattedDate := d.Month + "/" + d.Day + "/" + d.Year
return formattedDate, nil
}
The above function returns both the result of the operation and an error, if any. When there are no errors, the function returns the result along with a nil error.
Insights into the errors library:
The errors package implements several functions to handle errors efficiently in Go programming.
Variable
ErrUnsupportedThe ErrUnsupported indicates the operation is not supported. In the example below, the date format operation is not supported if any of the date values are empty.
Example: Continue from Part 7 of this series(
Part 7 - Useful Design Patterns for Go )
var ErrUnsupported = errors.New("unsupported operation")
// Function to receive Date struct instance and format the date
func (d *Date) Format() (string, error) {
if d.Month == "" || d.Day == "" || d.Year == "" {
return "", ErrUnsupported
}
formattedDate := d.Month + "/" + d.Day + "/" + d.Year
return formattedDate, nil
}
When calling the data format function, we can get formatted values and Unsupported errors if any. Update the date passed on the date format constructor and see the behavior of the function.
dateConstructor := dateformater.NewDate("2021", "07", "01")
formattedDate, err := dateConstructor.Format()
if err != nil {
if err == dateformater.ErrUnsupported {
fmt.Println("Unsupported operation:", err)
} else {
fmt.Println("Error:", err)
}
}
fmt.Println(formattedDate)
FunctionsAsThis function finds the first error in the error tree that matches the target. The target could be anything
i.e. custom error type, standard errors, etc.
Example- Continue from the previous section of this part.
Step 1. Add a new go file named 'date_custom_error.go' under the folder date_formater
date_custom_error.go
package dateformater
import (
"fmt"
)
type InvalidDateError struct {
Msg string
}
func (e *InvalidDateError) Error() string {
return fmt.Sprintf("unsupported operation becaue of invalid date format: %s", e.Msg)
}
// Constructor for the InvalidDateError.
func NewInvalidDateError(msg string) *InvalidDateError {
return &InvalidDateError{Msg: msg}
}
Step 2. Modify the date_formater.go and return the custom error type InvalidDateError.
package dateformater
type Date struct {
Year string
Month string
Day string
}
// Constructor for the Date struct.
func NewDate(year string, month string, day string) *Date {
return &Date{
Year: year,
Month: month,
Day: day,
}
}
// Function to receive Date struct instance and format the date
func (d *Date) Format() (string, error) {
if d.Month == "" || d.Day == "" || d.Year == "" {
return "", NewInvalidDateError("Missing date components")
}
formattedDate := d.Month + "/" + d.Day + "/" + d.Year
return formattedDate, nil
}
Step 3. Modify the main method and check if the custom error is present in the error tree or not.
fmt.Println("Crete new date instance using a constructor!")
dateConstructor := dateformater.NewDate("2021", "07", "")
formattedDate, err := dateConstructor.Format()
if err != nil {
var invalidDateErr *dateformater.InvalidDateError
if errors.As(err, &invalidDateErr) {
fmt.Println("Unsupported operation:", invalidDateErr)
} else {
fmt.Println("Error:", err)
}
//return
}
fmt.Println("Formatted date:", formattedDate)
Observe the above code and see how a custom error was created and utilized in the main method. Also, take note of the use of errors.As() function to check for a specific error type.
Is
This function checks if there is any error in the error tree. In the code below, the errors.Is(err, nil) function is used to determine whether the err variable contains any value or is simply nil.
dateConstructor := dateformater.NewDate("2021", "07", "")
formattedDate, err := dateConstructor.Format()
// Use error.Is() function to check if there is no error
if errors.Is(err, nil) {
fmt.Println("No error occurred.")
} else if err != nil {
var invalidDateErr *dateformater.InvalidDateError
if errors.As(err, &invalidDateErr) {
fmt.Println("Unsupported operation:", invalidDateErr)
} else {
fmt.Println("Error:", err)
}
//return
}
fmt.Println("Formatted date:", formattedDate)
Join
The join function wraps the error int error collection. the error collection will have the concatenated string line for searching individual errors.
Example: Update the main method to add errors on individual variables from the function return and at the end join them together.
unc main() {
fmt.Println("Crete new date instance using a constructor!")
dateConstructor := dateformater.NewDate("2021", "07", "01")
formattedDate, err1 := dateConstructor.Format()
// Use error.Is() function to check if there is no error
if errors.Is(err1, nil) {
fmt.Println("No error occurred.")
} else if err1 != nil {
var invalidDateErr *dateformater.InvalidDateError
if errors.As(err1, &invalidDateErr) {
fmt.Println("Unsupported operation:", invalidDateErr)
} else {
fmt.Println("Error:", err1)
}
//return
}
fmt.Println("Formatted date:", formattedDate)
//Create a channel to communicate and receive data between goroutines
ch := make(chan string)
var channelStatus bool
fmt.Println("Use name format package to format name!")
//get the person's names
persons, err2 := stringformater.GetPersonNames()
if err2 != nil {
fmt.Println("Exception occured to get persons")
panic(err2)
}
// Check if the address has values
if len(persons) > 0 {
// Loop through the collection and read properties
for _, person := range persons {
personName := &stringformater.PersonName{FirstName: person.FirstName, LastName: person.LastName}
go formatString(personName, ch)
formatedName, status := <-ch // receive formatted data from channel with channel status
channelStatus = status
fmt.Println(formatedName)
}
} else {
fmt.Println("No objects in the persons collection")
}
fmt.Println("Use name format package to format billing address!")
//get addresses
addresses, err3 := stringformater.GetAddress()
if err3 != nil {
fmt.Println("Exception occured to get address")
panic(err3)
}
// Check if the address has values
if len(addresses) > 0 {
// Loop through the collection and read properties
for _, addressInfo := range addresses {
address := &stringformater.Address{HouseNumber: addressInfo.HouseNumber, StreetName: addressInfo.StreetName, City: addressInfo.City, State: addressInfo.State, ZipCode: addressInfo.ZipCode}
go formatString(address, ch)
formatedAddress, status := <-ch // receive formatted data from channel with channel status
channelStatus = status
fmt.Println(formatedAddress)
}
} else {
fmt.Println("No objects in the address collection")
}
//Check the status of the channel
if channelStatus {
fmt.Println("Channel Ch is not closed!")
} else {
fmt.Println("Channel Ch is closed!")
}
// join errors together
joiinedError := errors.Join(err1, err2, err3)
fmt.Println(joiinedError)
}
New, New(Errorf)
This function is used to create a new error instance with the specified text and ErrorF(..) creates an error with formatted text. The 'New' function returns a new error instance, even if the same error message is used to create multiple error instances.
Example:
err := errors.New("Invalid data and time")
// new instance of error with formatted error text
err := fmt.Errorf("Invalid data and time: Month- %q, Day %q, and year %q", d.Month, d.Day, d.Year)
Unwrap
This function unwraps the individual error from the errors wrapped using fmt. Errorf function but it doesn't unwrap the errors wrapped using error.join
Example:
joiinedError := fmt.Errorf("Wrapping errors together: err1: %w, err2: %w, err3: %w", err1, err2, err3)
fmt.Println(joiinedError)
fmt.Println(errors.Unwrap(err1))
Defer, Panic, and Recover
Using defer schedules functions to be executed after the surrounding function returns. When conditions for execution are satisfied, functions that are deferred using defer and are in the differed list will be executed in, the First Out (LIFO) order.
Panic halts the execution of the current goroutine or process abruptly, akin to an exception in other languages.
recover allows a panicked goroutine to regain control and continue execution. It can be likened to the code executing within a catch block in languages like C# or Java.
Custom error
Golang allows to creation of custom errors as needed. This feature allows the engineers to define their own errors to handle the errors based on their software requirements.
Example:
date_custom_error.go
package dateformater
import (
"fmt"
)
type InvalidDateError struct {
Msg string
}
func (e *InvalidDateError) Error() string {
return fmt.Sprintf("unsupported operation becaue of invalid date format: %s", e.Msg)
}
// Constructor for the InvalidDateError.
func NewInvalidDateError(msg string) *InvalidDateError {
return &InvalidDateError{Msg: msg}
}
Uses of the above defined custom error:
// Function to receive Date struct instance and format the date
func (d *Date) Format() (string, error) {
if d.Month == "" || d.Day == "" || d.Year == "" {
return "", NewInvalidDateError("Missing date components")
}
formattedDate := d.Month + "/" + d.Day + "/" + d.Year
return formattedDate, nil
}
Reference
https://go.dev/blog/error-handling-and-go
https://pkg.go.dev/errors@go1.22.3
https://go.dev/blog/defer-panic-and-recover
No comments:
Post a Comment