Monday, May 20, 2024

Mastering Go: Part 8 - Error handling

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:

/ Function to receive Date struct instance and format the date 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 ErrUnsupported
The 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)

Functions

As
This 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

Mastering Go: Part 14 - Messaging with Apache Kafka(Go Implementation)

In this post, we will explore how to implement Apache Kafka messaging in Golang. Several packages are available, and the best choice depends...