Handling Errors in Go

To start this session, let’s look at how JavaScript, one of the most popular programming languages, handles errors.

1. JavaScript Error Handling

In JavaScript, you handle errors using try…catch blocks. This is straightforward for handling errors in a single function call, but it can become cumbersome when you need to handle errors from multiple functions separately. In such cases, you might need to nest try…catch blocks, which can make the code harder to read and maintain.
For example:
try {

    const candidate = getCandidate();

    try {

        const resume = getResume(candidate);

        console.log(resume);

    } catch (resumeError) {

        console.error("Error of getting resume:", resumeError);

    }

} catch (candidateError) {

    console.error("Error of getting candidate:", candidateError);

}

2. Go Basic Error Handling

In Go, error handling is done explicitly by checking the error returned by each function. This approach avoids deep nesting and keeps the error handling clear and concise. Each function call can be handled immediately, and the control flow remains simple.
Functions also can return multiple values, typically the desired result and an error. After defining the function name, we specify the types of the returned values, including an error.
When we call such a function, we check if the returned error is nil. If err is nil, it means the function executed successfully. If err is not nil, it indicates an error occurred, and we handle it accordingly.
Let’s analyze the example below
package main

import (

    "fmt"

    "io/ioutil"

    "strconv"

)

// readFile function attempts to read a file and return its contents

func readFile(filename string) (string, error) {

    data, err := ioutil.ReadFile(filename)

    if err != nil {

        return "", err

    }

    return string(data), nil
}

// parseInt function attempts to parse a string into an integer

func parseInt(s string) (int, error) {

    i, err := strconv.Atoi(s)

    if err != nil {

        return 0, err
    }

    return i, nil
}


func main() {

    // Example usage of readFile

    content, err := readFile("example.txt")

    if err != nil {

        fmt.Println("Error reading file:", err)

    } else {

        fmt.Println("File contents:", content)

    }

    // Example usage of parseInt

    number, err := parseInt(content)

    if err != nil {

        fmt.Println("Error parsing integer:", err)

    } else {

        fmt.Println("Parsed integer:", number)

    }

}
In the code snippet above, we have two functions: readFile() and parseInt().
The readFile() function reads the contents of a file specified by its filename and returns the content as a string. Errors such as ‘File Not Found’ or ‘Permission Denied’ can occur during this operation.
The parseInt() function attempts to convert a string into an integer. It may encounter an error if the string contains non-numeric characters, resulting in an ‘invalid syntax’ error.
In the main() function, we demonstrate using readFile() to read from a file and then pass that content to parseInt() to convert it into an integer. Each function handles its specific errors independently, ensuring the code remains concise and focused on error handling specific to file operations and string parsing

3. Creating Custom Errors

Go also allows us to custom errors
The error interface in Go is defined as follows:
type error interface {

    Error() string

}
To understand better, let’s create and use a custom error.
package main

import "fmt"

// Define a custom error type

type customError struct {

    Code    int

    Message string

}

// Implement the Error() method for the customError type

func (e *customError) Error() string {

    return fmt.Sprintf("Error %d: %s", e.Code, e.Message)
}

// Function that checks the candidate type and returns a custom error if invalid

func checkCandidateType(candidateType string) error {

    if candidateType != "A" && candidateType != "B" {

        return &customError{

            Code:    400,

            Message: "Invalid candidate type",

        }

    }

    return nil

}

func main() {

    err := checkCandidateType("C")

    if err != nil {

        // Print the custom error

        fmt.Println(err)

        return

    }

    fmt.Println("Candidate type is valid")

}
Let’s explore the code
Custom Error Type (customError):
We defined a custom error type customError with two fields: Code (int) and Message (string)
Error Method Implementation:
The Error() method is implemented for the customError struct. This method formats and returns the error message string using fmt.Sprintf
The function for checking candidate type (checkCandidateType()):
This function checks if the candidate type (candidateType) is either “A” or “B”.
If it’s neither “A” nor “B”, it returns a pointer to a customError with Code set to 400 and Message set to “Invalid candidate type”.
Otherwise, it returns nil, indicating no error.
The main() Function:
In main, you call checkCandidateType(“C”).
We check if err is not nil and print the error message using fmt.Println(err).
If err is nil, it prints “Candidate type is valid”.

4. Create a New Error With a Specified Message

errors.New is used to create a simple, static error message that doesn’t need formatting and is known at compile-time.
func Sqrt(f float64) (float64, error) {

    if f < 0 {

        return 0, errors.New("math: square root of negative number")

    }

    return math.Sqrt(f), nil

}

5. Create Formatted Error Messages

fmt.Errorf() is used to construct error messages dynamically or include variable values in your error messages.
func Sqrt(f float64) (float64, error) {

    if f < 0 {
        return 0, fmt.Errorf("math: square root of negative number (%f)", f)
    }
    return math.Sqrt(f), nil
}

6. Defer, Panic, and Recover

Defer
In Go, defer is a keyword that is used to schedule a function call to be executed just before the surrounding function returns.
This example helps us understand the importance of defer in Go. The function CopyFile opens the srcName file, creates the dstName file, and then copies the contents from srcName to dstName.
(The code snippet derived from https://go.dev/blog/defer-panic-and-recover)
Without defer usage, if the creation (os.Create) of the destination file (dstName) fails, the function might return without closing the source file (srcName)
func CopyFile(dstName, srcName string) (written int64, err error) {

    src, err := os.Open(srcName)

    if err != nil {

        return

    }

    dst, err := os.Create(dstName)

    if err != nil {

        return

    }

    written, err = io.Copy(dst, src)

    dst.Close()

    src.Close()

    return

}
However, with defer usage as shown in the function:
func CopyFile(dstName, srcName string) (written int64, err error) {

    src, err := os.Open(srcName)

    if err != nil {

        return

    }

    defer src.Close()

    dst, err := os.Create(dstName)

    if err != nil {

        return

    }

    defer dst.Close()

    return io.Copy(dst, src)

}
Both srcName and dstName files are guaranteed to be closed after the CopyFile function completes, whether it exits normally or due to an error. This ensures proper cleanup of resources and prevents resource leaks.
In Go, the panic and recover mechanisms are used to handle exceptional situations, especially those that might otherwise cause the program to terminate abruptly. Here’s a breakdown of how they work together:
Panic
You can trigger a panic explicitly using the panic() function. This stops normal execution of the current function and begins to unwind the stack.
Panics are typically used for critical errors or unexpected conditions where continuing normal execution is not feasible or safe. Example include division by zero, attempting to access an out-of-bounds array index, or encountering a nil pointer dereference.
Recover
recover() is a built-in function in Go that allows you to regain control of a panicking goroutine. It returns the value passed to panic() if a panic occurred, or nil if there was no panic. It also is typically used in conjunction with defer to handle panics gracefully.
Functions deferred using defer are executed in Last In, First Out (LIFO) order when the surrounding function returns.
Here’s an example to illustrate how panic, recover, and defer work together:
package main

import "fmt"

func recoverFunction() {

    if r := recover(); r != nil {

        fmt.Println("Recovered from panic:", r)

    }

}

func doSomethingCritical() {

    defer recoverFunction()

    fmt.Println("Doing something critical...")

    // Simulate a panic

    panic("something went wrong")

}

func main() {

    fmt.Println("Start program")

    doSomethingCritical()

    fmt.Println("Program continues after doSomethingCritical()")

}
In recoverFunction(), recover() is used to check if a panic has occurred with if r := recover(); r != nil. If a panic occurred, r will hold the value that was passed to panic(“something went wrong”), such as ‘something went wrong’ in this example.
By placing a call to recover() inside a deferred function (recoverFunction), you can capture and handle panics that occur within the surrounding function (doSomethingCritical)
We’ve covered essential error handling concepts: basic error handling, custom errors, panic, recover, and defer.
Keep learning and enjoy mastering these techniques!

関連記事

カテゴリー:

ブログ

情シス求人

  1. 登録されている記事はございません。
ページ上部へ戻る