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 benil
. 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