Error handling in Go is somewhat unique compared to some other programming languages. Instead of using exceptions, Go uses error values to indicate an abnormal state. This design promotes explicit error checking and can lead to more robust code.
1. The error
Type:
Go has a built-in interface type called error
. This interface has a single method Error() string
.
type error interface {
Error() string
}
Any type that implements this method can be used as an error.
2. Returning Errors:
It's idiomatic in Go to return errors from functions and methods. A common pattern is to return both a result and an error, and to check the error immediately:
func SomeFunction() (ResultType, error) {
if somethingWrong {
return nil, errors.New("something went wrong")
}
return result, nil
}
result, err := SomeFunction()
if err != nil {
// handle the error
}
3. Custom Error Types:
Custom error types can be particularly useful when you want to provide more information about an error or categorize errors in a way that allows the caller to programmatically react to different error scenarios.
Here's an example of how you might use custom error types in Go:
package main
import (
"fmt"
)
// NotFoundError represents an error when something is not found.
type NotFoundError struct {
EntityName string
}
func (e *NotFoundError) Error() string {
return fmt.Sprintf("%s not found", e.EntityName)
}
// PermissionError represents an error when permissions are denied.
type PermissionError struct {
OperationName string
}
func (e *PermissionError) Error() string {
return fmt.Sprintf("permission denied for %s", e.OperationName)
}
Using and Checking Custom Error Types:
func FindEntity(name string) (string, error) {
// Simulating some logic
if name == "missingEntity" {
return "", &NotFoundError{EntityName: name}
}
return "EntityData", nil
}
func PerformOperation(name string) error {
// Simulating some logic
if name == "restrictedOperation" {
return &PermissionError{OperationName: name}
}
return nil
}
func main() {
_, err := FindEntity("missingEntity")
if err != nil {
switch e := err.(type) {
case *NotFoundError:
fmt.Println("Handle not found:", e.Error())
case *PermissionError:
fmt.Println("Handle permission error:", e.Error())
default:
fmt.Println("Handle general error:", e.Error())
}
}
err = PerformOperation("restrictedOperation")
if err != nil {
switch e := err.(type) {
case *NotFoundError:
fmt.Println("Handle not found:", e.Error())
case *PermissionError:
fmt.Println("Handle permission error:", e.Error())
default:
fmt.Println("Handle general error:", e.Error())
}
}
}
In this example, we have two custom error types: NotFoundError
and PermissionError
. When we encounter these errors in our functions (FindEntity
and PerformOperation
), we return instances of these types. In our main function, we can use a type switch to handle different types of errors in different ways.
This approach provides flexibility and clarity in error handling, and allows callers to react differently based on the type of error encountered.
4. The fmt
Package for Errors:
The fmt
package can be used to format error strings. The Errorf function is particularly handy for this:
return fmt.Errorf("error occurred while processing %s", itemName)
5. Wrapping Errors:
As of Go 1.13, the standard library added support for wrapping errors using the %w
verb with fmt.Errorf
. This allows you to wrap an error with additional context, while preserving the original error:
if err != nil {
return fmt.Errorf("failed processing: %w", err)
}
You can then later unwrap and check the underlying error using errors.Is
and errors.As
.
6. Checking Errors:
The errors
package provides utilities to work with errors.
errors.Is
: Check if an error is a specific error or wrapped version of it.
errors.As
: Check if an error matches a certain type and, if so, get the typed value.
var specificErr MyError
if errors.As(err, &specificErr) {
// handle specificErr
}
Best Practices:
- Always handle errors explicitly. Don't ignore them unless you're certain it's safe to do so.
- When wrapping errors, provide context so that the root cause can be traced.
- Avoid having too many error return values. If a function returns multiple errors, consider if there's a way to simplify or break up the function.
- Use custom error types judiciously. Often, a simple error string is sufficient.
By following these practices and understanding Go's unique approach to error handling, you can write more resilient and maintainable code.
Top comments (0)