Defer and Error Handling

Defer

The defer statement in Go is used to execute a function call just before the enclosing function returns. 

Example 1: Basic Usage of Defer

package main

import (
	"fmt"
	"time"
)

func logExecutionTime(start time.Time) {
	fmt.Printf("Execution time: %.2f seconds\n", time.Since(start).Seconds())
}

func performTask() {
	start := time.Now()
	defer logExecutionTime(start)
	time.Sleep(3 * time.Second)
	fmt.Println("Task completed")
}

func main() {
	performTask()
}

Explanation:
In this program, defer is used to measure the time taken by the performTask function. The start time is passed to the deferred call to logExecutionTime, which gets executed just before the function exits.

Output:

Task completed
Execution time: 3.00 seconds
Arguments Evaluation

The arguments of a deferred function are evaluated when the defer statement is executed, not at the time of function execution.

Example 2: Argument Evaluation

package main

import "fmt"

func printValue(x int) {
	fmt.Println("Deferred function received value:", x)
}

func main() {
	y := 7
	defer printValue(y)
	y = 15
	fmt.Println("Updated value of y before deferred execution:", y)
}

Explanation:
Here, y initially holds the value 7. When the defer statement is executed, the value of y at that moment is captured (7). Later, even though y is updated to 15, the deferred call uses the value captured at the time the defer statement was executed.

Output:

Updated value of y before deferred execution: 15
Deferred function received value: 7
Deferred Methods

Defer works not just with functions but also with methods.

Example 3: Deferred Method

package main

import "fmt"

type animal struct {
	name string
	kind string
}

func (a animal) describe() {
	fmt.Printf("%s is a %s.\n", a.name, a.kind)
}

func main() {
	dog := animal{name: "Buddy", kind: "Dog"}
	defer dog.describe()
	fmt.Println("Starting program")
}

Output:

Starting program
Buddy is a Dog.
Stacking Multiple Defers

Deferred calls are executed in Last In, First Out (LIFO) order.

Example 4: Reversing a String Using Deferred Calls

package main

import "fmt"

func main() {
	word := "Hello"
	fmt.Printf("Original Word: %s\n", word)
	fmt.Printf("Reversed Word: ")
	for _, char := range word {
		defer fmt.Printf("%c", char)
	}
}

Explanation:
Each deferred call to fmt.Printf is pushed onto a stack. When the function exits, these calls are executed in reverse order, printing the string backward.

Output:

Original Word: Hello
Reversed Word: olleH
Practical Uses of Defer

Defer is especially useful in scenarios where a function call must be executed regardless of the flow of the program.

Example 5: Simplified WaitGroup Implementation

package main

import (
	"fmt"
	"sync"
)

type rectangle struct {
	length int
	width  int
}

func (r rectangle) calculateArea(wg *sync.WaitGroup) {
	defer wg.Done()
	if r.length <= 0 || r.width <= 0 {
		fmt.Printf("Invalid dimensions for rectangle: %+v\n", r)
		return
	}
	fmt.Printf("Area of rectangle %+v: %d\n", r, r.length*r.width)
}

func main() {
	var wg sync.WaitGroup
	rects := []rectangle{
		{length: 10, width: 5},
		{length: -8, width: 4},
		{length: 6, width: 0},
	}

	for _, rect := range rects {
		wg.Add(1)
		go rect.calculateArea(&wg)
	}

	wg.Wait()
	fmt.Println("All goroutines completed")
}

Explanation:
The defer wg.Done() ensures the Done call is executed no matter how the function exits. This simplifies the code, making it easier to read and maintain.

Output:

Area of rectangle {length:10 width:5}: 50
Invalid dimensions for rectangle: {length:-8 width:4}
Invalid dimensions for rectangle: {length:6 width:0}
All goroutines completed

Error Handling

Errors indicate abnormal conditions occurring in a program. For example, if you try to open a file that does not exist, it leads to an error. In Go, errors are values just like int, float64, etc. They can be stored in variables, passed as parameters, or returned from functions. Errors in Go are represented using the built-in error type.

Example:

package main

import (
	"fmt"
	"os"
)

func main() {
	file, err := os.Open("nonexistent.txt")
	if err != nil {
		fmt.Println("Error occurred:", err)
		return
	}
	fmt.Println(file.Name(), "opened successfully")
}

Explanation:

  • The os.Open function attempts to open a file. It returns two values: a file handle and an error.
  • If the file does not exist, err will not be nil. Hence, the program prints the error message and exits.

Output:

Error occurred: open nonexistent.txt: no such file or directory
The error Type

The error type is an interface defined as follows:

type error interface {
    Error() string
}

Any type implementing the Error() method is considered an error. When you use fmt.Println with an error, it internally calls the Error() method to print the description.

Extracting More Information from Errors

1. Converting Errors to Structs: Many errors in Go are returned as struct types that implement the error interface. For example, the os.Open function may return an error of type *os.PathError. The *os.PathError struct is defined as:

type PathError struct {
    Op   string
    Path string
    Err  error
}

func (e *PathError) Error() string {
    return e.Op + " " + e.Path + ": " + e.Err.Error()
}

To retrieve more information, you can use the errors.As function:

package main

import (
	"errors"
	"fmt"
	"os"
)

func main() {
	_, err := os.Open("invalidfile.txt")
	if err != nil {
		var pathErr *os.PathError
		if errors.As(err, &pathErr) {
			fmt.Println("Error occurred while accessing:", pathErr.Path)
			return
		}
		fmt.Println("General error:", err)
	}
}

Output:

Error occurred while accessing: invalidfile.txt

2. Using Methods on Structs: Some error structs have additional methods. For example, the net.DNSError struct provides methods to check if the error is due to a timeout or is temporary:

Example:

package main

import (
	"errors"
	"fmt"
	"net"
)

func main() {
	_, err := net.LookupHost("invalidhost.example")
	if err != nil {
		var dnsErr *net.DNSError
		if errors.As(err, &dnsErr) {
			if dnsErr.Timeout() {
				fmt.Println("Operation timed out")
			} else if dnsErr.Temporary() {
				fmt.Println("Temporary DNS error")
			} else {
				fmt.Println("Generic DNS error:", dnsErr)
			}
			return
		}
		fmt.Println("Other error:", err)
	}
}

Output:

Generic DNS error: lookup invalidhost.example: no such host

3. Direct Comparison: Some errors are defined as variables in the standard library, allowing direct comparison. For example, the filepath.Glob function returns the filepath.ErrBadPattern error if the pattern is invalid:

package main

import (
	"errors"
	"fmt"
	"path/filepath"
)

func main() {
	_, err := filepath.Glob("[")
	if err != nil {
		if errors.Is(err, filepath.ErrBadPattern) {
			fmt.Println("Invalid pattern:", err)
			return
		}
		fmt.Println("Other error:", err)
	}
}

Output:

No tasks are ready yet
Ignoring Errors (Not Recommended)

Ignoring errors can lead to unexpected behavior. 

Example:

package main

import (
	"fmt"
	"path/filepath"
)

func main() {
	files, _ := filepath.Glob("[")
	fmt.Println("Matched files:", files)
}

Output:

Matched files: []

Custom Errors

Lets learn how to create custom errors for functions and packages, using techniques inspired by the standard library to provide detailed error information.

Creating Custom Errors with the New Function

The simplest way to create a custom error is by using the New function from the errors package. Before we use it, let’s examine its implementation in the errors package:

package errors

// New returns an error that formats as the given text.
// Each call to New returns a distinct error value even if the text is identical.
func New(text string) error {
    return &errorString{text}
}

// errorString is a trivial implementation of error.
type errorString struct {
    s string
}

func (e *errorString) Error() string {
    return e.s
}

This implementation is straightforward. The errorString struct contains a single field s for the error message. The Error() method implements the error interface. The New function creates an errorString value, takes its address, and returns it as an error.

Example: Validating a Triangle’s Sides

Let’s write a program to validate whether three given sides can form a triangle. If any side is negative, the function will return an error.

package main

import (
	"errors"
	"fmt"
)

func validateTriangle(a, b, c float64) error {
	if a < 0 || b < 0 || c < 0 {
		return errors.New("Triangle validation failed: one or more sides are negative")
	}
	return nil
}

func main() {
	a, b, c := 3.0, -4.0, 5.0
	err := validateTriangle(a, b, c)
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println("The triangle sides are valid.")
}

Output:

Triangle validation failed: one or more sides are negative
Adding More Details Using Errorf

To provide more information, such as which side caused the error, we can use the Errorf function from the fmt package. It formats the error string with placeholders.

Example: Using Errorf to Identify Invalid Side

package main

import (
	"fmt"
)

func validateTriangle(a, b, c float64) error {
	if a < 0 {
		return fmt.Errorf("Triangle validation failed: side a (%0.2f) is negative", a)
	}
	if b < 0 {
		return fmt.Errorf("Triangle validation failed: side b (%0.2f) is negative", b)
	}
	if c < 0 {
		return fmt.Errorf("Triangle validation failed: side c (%0.2f) is negative", c)
	}
	return nil
}

func main() {
	a, b, c := 3.0, -4.0, 5.0
	err := validateTriangle(a, b, c)
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println("The triangle sides are valid.")
}
Triangle validation failed: side b (-4.00) is negative
Using Struct Types for Detailed Errors

Struct types can add flexibility, allowing access to fields with specific error-related information. This approach eliminates the need to parse error strings.

Example: Custom Error with Struct Fields

package main

import (
	"errors"
	"fmt"
)

type triangleError struct {
	sideName string
	sideValue float64
	err       string
}

func (e *triangleError) Error() string {
	return fmt.Sprintf("Triangle validation failed: side %s (%0.2f) is invalid - %s", e.sideName, e.sideValue, e.err)
}

func validateTriangle(a, b, c float64) error {
	if a < 0 {
		return &triangleError{"a", a, "side is negative"}
	}
	if b < 0 {
		return &triangleError{"b", b, "side is negative"}
	}
	if c < 0 {
		return &triangleError{"c", c, "side is negative"}
	}
	return nil
}

func main() {
	a, b, c := 3.0, -4.0, 5.0
	err := validateTriangle(a, b, c)
	if err != nil {
		var tErr *triangleError
		if errors.As(err, &tErr) {
			fmt.Printf("Error: side %s is invalid, value: %0.2f\n", tErr.sideName, tErr.sideValue)
			return
		}
		fmt.Println(err)
		return
	}
	fmt.Println("The triangle sides are valid.")
}
Error: side b is invalid, value: -4.00
Using Methods for Additional Insights

Methods on the custom error type can provide specific insights.

Example: Identifying Invalid Sides

package main

import (
	"errors"
	"fmt"
)

type triangleError struct {
	err       string
	a, b, c   float64
}

func (e *triangleError) Error() string {
	return e.err
}

func (e *triangleError) isSideANegative() bool {
	return e.a < 0
}

func (e *triangleError) isSideBNegative() bool {
	return e.b < 0
}

func (e *triangleError) isSideCNegative() bool {
	return e.c < 0
}

func validateTriangle(a, b, c float64) error {
	err := ""
	if a < 0 {
		err += "side a is negative"
	}
	if b < 0 {
		if err != "" {
			err += ", "
		}
		err += "side b is negative"
	}
	if c < 0 {
		if err != "" {
			err += ", "
		}
		err += "side c is negative"
	}
	if err != "" {
		return &triangleError{err, a, b, c}
	}
	return nil
}

func main() {
	a, b, c := -3.0, -4.0, 5.0
	err := validateTriangle(a, b, c)
	if err != nil {
		var tErr *triangleError
		if errors.As(err, &tErr) {
			if tErr.isSideANegative() {
				fmt.Printf("Error: side a (%0.2f) is negative\n", tErr.a)
			}
			if tErr.isSideBNegative() {
				fmt.Printf("Error: side b (%0.2f) is negative\n", tErr.b)
			}
			if tErr.isSideCNegative() {
				fmt.Printf("Error: side c (%0.2f) is negative\n", tErr.c)
			}
			return
		}
		fmt.Println(err)
		return
	}
	fmt.Println("The triangle sides are valid.")
}
Error: side a (-3.00) is negative
Error: side b (-4.00) is negative

Error Wrapping

Understanding Error Wrapping

Error wrapping involves encapsulating one error into another. Imagine we have a web service that accesses a database to fetch a record. If the database call results in an error, we can choose to wrap this error or return a custom error message. Let’s look at an example to clarify:

package main

import (
	"errors"
	"fmt"
)

var recordNotFound = errors.New("record not found")

func fetchRecord() error {
	return recordNotFound
}

func serviceHandler() error {
	if err := fetchRecord(); err != nil {
		return fmt.Errorf("Service Error: %s during database query", err)
	}
	return nil
}

func main() {
	if err := serviceHandler(); err != nil {
		fmt.Printf("Service failed with: %s\n", err)
		return
	}
	fmt.Println("Service executed successfully")
}

In this example, we send a string representation of the error encountered in fetchRecord back from serviceHandler

Error Wrapping with errors.Is

The Is function in the errors package checks whether any error in the chain matches a target error. In the previous example, the error from fetchRecord is returned as a formatted string from serviceHandler, which breaks error wrapping. Let’s modify the main function to demonstrate:

func main() {
	if err := serviceHandler(); err != nil {
		if errors.Is(err, recordNotFound) {
			fmt.Printf("The record cannot be retrieved. Database error: %s\n", err)
			return
		}
		fmt.Println("An unknown error occurred during record retrieval")
		return
	}
	fmt.Println("Service executed successfully")
}

Here, the Is function in line 4 checks whether any error in the chain matches the recordNotFound error. However, this won’t work because the error isn’t wrapped correctly. To fix this, we can use the %w format specifier to wrap the error properly.

Modify the error return in serviceHandler to:

return fmt.Errorf("Service Error: %w during database query", err)

The complete corrected program:

package main

import (
	"errors"
	"fmt"
)

var recordNotFound = errors.New("record not found")

func fetchRecord() error {
	return recordNotFound
}

func serviceHandler() error {
	if err := fetchRecord(); err != nil {
		return fmt.Errorf("Service Error: %w during database query", err)
	}
	return nil
}

func main() {
	if err := serviceHandler(); err != nil {
		if errors.Is(err, recordNotFound) {
			fmt.Printf("The record cannot be retrieved. Database error: %s\n", err)
			return
		}
		fmt.Println("An unknown error occurred during record retrieval")
		return
	}
	fmt.Println("Service executed successfully")
}

Output:

The record cannot be retrieved. Database error: Service Error: record not found during database query
Error Wrapping with errors.As

The As function in the errors package attempts to convert an error to a target type. If successful, it sets the target to the first matching error in the chain and returns true. Example:

package main

import (
	"errors"
	"fmt"
)

type ServiceError struct {
	message string
}

func (e ServiceError) Error() string {
	return e.message
}

func fetchRecord() error {
	return ServiceError{
		message: "record not found",
	}
}

func serviceHandler() error {
	if err := fetchRecord(); err != nil {
		return fmt.Errorf("Service Error: %w during database query", err)
	}
	return nil
}

func main() {
	if err := serviceHandler(); err != nil {
		var svcErr ServiceError
		if errors.As(err, &svcErr) {
			fmt.Printf("Record retrieval failed. Error details: %s\n", svcErr)
			return
		}
		fmt.Println("An unexpected error occurred during record retrieval")
		return
	}
	fmt.Println("Service executed successfully")
}

In this example, the fetchRecord function returns a custom error of type ServiceError. The errors.As function in line 27 attempts to cast the error returned from serviceHandler into the ServiceError type. If successful, it prints the error message.

Output:

Record retrieval failed. Error details: record not found

Panic and Recover

Understanding Error Wrapping

Error wrapping involves encapsulating one error into another. Imagine we have a web service that accesses a database to fetch a record. If the database call results in an error, we can choose to wrap this error or return a custom error message.

Example:

package main

import (
	"errors"
	"fmt"
)

var recordNotFound = errors.New("record not found")

func fetchRecord() error {
	return recordNotFound
}

func serviceHandler() error {
	if err := fetchRecord(); err != nil {
		return fmt.Errorf("Service Error: %s during database query", err)
	}
	return nil
}

func main() {
	if err := serviceHandler(); err != nil {
		fmt.Printf("Service failed with: %s\n", err)
		return
	}
	fmt.Println("Service executed successfully")
}

In this example, we send a string representation of the error encountered in fetchRecord back from serviceHandler.

Error Wrapping with errors.Is

The Is function in the errors package checks whether any error in the chain matches a target error. In the previous example, the error from fetchRecord is returned as a formatted string from serviceHandler, which breaks error wrapping. Let’s modify the main function to demonstrate:

func main() {
	if err := serviceHandler(); err != nil {
		if errors.Is(err, recordNotFound) {
			fmt.Printf("The record cannot be retrieved. Database error: %s\n", err)
			return
		}
		fmt.Println("An unknown error occurred during record retrieval")
		return
	}
	fmt.Println("Service executed successfully")
}

Here, the Is function in line 4 checks whether any error in the chain matches the recordNotFound error. However, this won’t work because the error isn’t wrapped correctly. To fix this, we can use the %w format specifier to wrap the error properly.

Modify the error return in serviceHandler to:

return fmt.Errorf("Service Error: %w during database query", err)

The complete corrected program:

package main

import (
	"errors"
	"fmt"
)

var recordNotFound = errors.New("record not found")

func fetchRecord() error {
	return recordNotFound
}

func serviceHandler() error {
	if err := fetchRecord(); err != nil {
		return fmt.Errorf("Service Error: %w during database query", err)
	}
	return nil
}

func main() {
	if err := serviceHandler(); err != nil {
		if errors.Is(err, recordNotFound) {
			fmt.Printf("The record cannot be retrieved. Database error: %s\n", err)
			return
		}
		fmt.Println("An unknown error occurred during record retrieval")
		return
	}
	fmt.Println("Service executed successfully")
}

Output:

The record cannot be retrieved. Database error: Service Error: record not found during database query
Error Wrapping with errors.As

The As function in the errors package attempts to convert an error to a target type. If successful, it sets the target to the first matching error in the chain and returns true.

Example:

package main

import (
	"errors"
	"fmt"
)

type ServiceError struct {
	message string
}

func (e ServiceError) Error() string {
	return e.message
}

func fetchRecord() error {
	return ServiceError{
		message: "record not found",
	}
}

func serviceHandler() error {
	if err := fetchRecord(); err != nil {
		return fmt.Errorf("Service Error: %w during database query", err)
	}
	return nil
}

func main() {
	if err := serviceHandler(); err != nil {
		var svcErr ServiceError
		if errors.As(err, &svcErr) {
			fmt.Printf("Record retrieval failed. Error details: %s\n", svcErr)
			return
		}
		fmt.Println("An unexpected error occurred during record retrieval")
		return
	}
	fmt.Println("Service executed successfully")
}

In this example, the fetchRecord function returns a custom error of type ServiceError. The errors.As function in line 27 attempts to cast the error returned from serviceHandler into the ServiceError type. If successful, it prints the error message.

Output:

Record retrieval failed. Error details: record not found

Output:

Hello
World
Go
Channel Properties

1.Length of a Channel: Use len() to find the number of elements currently in the channel.

Example:

// Go program to find channel length
package main

import "fmt"

func main() {
    ch := make(chan int, 3)

    ch <- 10
    ch <- 20

    fmt.Println("Channel length:", len(ch))
}

Output:

Channel length: 2

2. Capacity of a Channel: Use cap() to find the total capacity of the channel.

Example:

// Go program to find channel capacity
package main

import "fmt"

func main() {
    ch := make(chan float64, 4)

    ch <- 1.1
    ch <- 2.2

    fmt.Println("Channel capacity:", cap(ch))
}

Output:

Channel capacity: 4

Unidirectional Channel in Golang

In Golang, channels act as a communication mechanism between concurrently running goroutines, allowing them to transmit and receive data. By default, channels in Go are bidirectional, meaning they support both sending and receiving operations. However, it’s possible to create unidirectional channels, which can either exclusively send or receive data. These unidirectional channels can be constructed using the make() function as demonstrated below:

// For receiving data only
c1 := make(<-chan bool)

// For sending data only
c2 := make(chan<- bool)

Example:

// Go program to demonstrate the concept
// of unidirectional channels
package main

import "fmt"

func main() {
    // Channel restricted to receiving data
    recvOnly := make(<-chan int)

    // Channel restricted to sending data
    sendOnly := make(chan<- int)

    // Display the types of the channels
    fmt.Printf("%T", recvOnly)
    fmt.Printf("\n%T", sendOnly)
}

Output:

<-chan int
chan<- int
Converting Bidirectional Channels into Unidirectional Channels

In Go, you can convert a bidirectional channel into a unidirectional channel, meaning you can restrict it to either sending or receiving data. However, the reverse conversion (from unidirectional back to bidirectional) is not possible. This concept is illustrated in the following example:

Example:

// Go program to illustrate conversion
// of a bidirectional channel into a
// unidirectional channel
package main

import "fmt"

// Function to send data through a send-only channel
func sendData(channel chan<- string) {
    channel <- "Hello from Golang"
}

func main() {
    // Creating a bidirectional channel
    bidiChannel := make(chan string)

    // Passing the bidirectional channel to a function,
    // which restricts it to a send-only channel
    go sendData(bidiChannel)

    // Receiving data from the channel
    fmt.Println(<-bidiChannel)
}

Output:

Hello from Golang