1. The Basics: Errors in Go
What is an Error?
In Go, an error is a value indicating that something went wrong. Think of it as a signal saying, "Oops, something didn't go as planned!"
Basic Error Handling
Functions in Go often return two values: the result and an error. For example:
package main
import (
"fmt"
"errors"
)
func divide(a, b int) (int, error) {
if b == 0 {
return 0, errors.New("cannot divide by zero")
}
return a / b, nil
}
func main() {
result, err := divide(10, 0)
if err != nil {
fmt.Println("Error:", err)
} else {
fmt.Println("Result:", result)
}
}
divide(10, 0)
returns an error because you can't divide by zero.if err != nil
checks if there's an error and handles it.
2. Custom Errors
Sometimes, you need more detailed error messages. You can create custom errors using fmt.Errorf
.
package main
import (
"fmt"
)
func divide(a, b int) (int, error) {
if b == 0 {
return 0, fmt.Errorf("divide: cannot divide %d by %d", a, b)
}
return a / b, nil
}
func main() {
result, err := divide(10, 0)
if err != nil {
fmt.Println("Error:", err)
} else {
fmt.Println("Result:", result)
}
}
3. The Error Interface
In Go, the error
interface is a built-in type that represents an error condition. It is defined as:
type error interface {
Error() string
}
Any type that implements the Error
method can be used as an error. This allows you to create custom error types.
4. Custom Error Types
For more advanced error handling, you can define your own error types.
package main
import (
"fmt"
)
type DivideError struct {
Dividend int
Divisor int
}
func (e *DivideError) Error() string {
return fmt.Sprintf("cannot divide %d by %d", e.Dividend, e.Divisor)
}
func divide(a, b int) (int, error) {
if b == 0 {
return 0, &DivideError{a, b}
}
return a / b, nil
}
func main() {
result, err := divide(10, 0)
if err != nil {
if divErr, ok := err.(*DivideError); ok {
fmt.Println("Divide error:", divErr)
} else {
fmt.Println("Error:", err)
}
} else {
fmt.Println("Result:", result)
}
}
DivideError
is a custom error type.It implements the
Error
method from theerror
interface.
5. Wrapping Errors
Go 1.13 introduced error wrapping, allowing you to retain the original error context.
package main
import (
"fmt"
"errors"
)
func openFile(filename string) error {
return fmt.Errorf("openFile: %w", errors.New("file not found"))
}
func main() {
err := openFile("test.txt")
if err != nil {
fmt.Println("Error:", err)
}
}
%w
is used to wrap the original error.You can then use
errors.Unwrap
to retrieve the original error.
package main
import (
"fmt"
"errors"
)
func openFile(filename string) error {
originalErr := errors.New("file not found")
return fmt.Errorf("openFile: %w", originalErr)
}
func main() {
err := openFile("test.txt")
if err != nil {
fmt.Println("Error:", err)
unwrappedErr := errors.Unwrap(err)
fmt.Println("Unwrapped Error:", unwrappedErr)
}
}
// Error: openFile: file not found
// Unwrapped Error: file not found
6. Error Chaining
You can chain errors to add more context at different levels of your program. Here's an example with three levels of chaining:
package main
import (
"fmt"
"errors"
)
func readFile() error {
return errors.New("readFile: file not found")
}
func parseData() error {
err := readFile()
if err != nil {
return fmt.Errorf("parseData: %w", err)
}
return nil
}
func processData() error {
err := parseData()
if err != nil {
return fmt.Errorf("processData: %w", err)
}
return nil
}
func main() {
err := processData()
if err != nil {
fmt.Println("Error:", err)
unwrappedErr := errors.Unwrap(err)
fmt.Println("Unwrapped Error:", unwrappedErr)
secondUnwrap := errors.Unwrap(unwrappedErr)
fmt.Println("Second Unwrapped Error:", secondUnwrap)
}
}
// Error: processData: parseData: readFile: file not found
// Unwrapped Error: parseData: readFile: file not found
// Second Unwrapped Error: readFile: file not found
7. Checking Error Types: errors.Is
vs errors.As
errors.Is
errors.Is
checks if an error matches a specific error value. This is useful for comparing against sentinel errors.
package main
import (
"fmt"
"errors"
)
var ErrDivideByZero = errors.New("cannot divide by zero")
func divide(a, b int) (int, error) {
if b == 0 {
return 0, ErrDivideByZero
}
return a / b, nil
}
func main() {
result, err := divide(10, 0)
if errors.Is(err, ErrDivideByZero) {
fmt.Println("Error: cannot divide by zero")
} else {
fmt.Println("Result:", result)
}
}
errors.As
errors.As
checks if an error can be cast to a specific type. This is useful for working with custom error types.
package main
import (
"fmt"
"errors"
)
type DivideError struct {
Dividend int
Divisor int
}
func (e *DivideError) Error() string {
return fmt.Sprintf("cannot divide %d by %d", e.Dividend, e.Divisor)
}
func divide(a, b int) (int, error) {
if b == 0 {
return 0, &DivideError{a, b}
}
return a / b, nil
}
func main() {
result, err := divide(10, 0)
if err != nil {
var divErr *DivideError
if errors.As(err, &divErr) {
fmt.Printf("Divide error: %v (Dividend: %d, Divisor: %d)\n", divErr, divErr.Dividend, divErr.Divisor)
} else {
fmt.Println("Error:", err)
}
} else {
fmt.Println("Result:", result)
}
}