Error Handling in Go: Philosophy and Practice
Unlike many modern languages, Go (Golang) does not use the classic try/catch construct. Instead, Go developers adhere to the "errors as values" approach. This makes code more explicit, predictable, and safe. In this article, we will cover all aspects of error handling in Go: from basic patterns to advanced techniques with defer, panic, and recover.
1. Basics: Errors Are Just Values
In Go, an error is an interface error with a single method Error() string. Any function that can fail returns an error as its last return value.
package main
import ( "errors" "fmt")
// Division function returning an errorfunc divide(a, b float64) (float64, error) { if b == 0 { return 0, errors.New("division by zero is not allowed") } return a / b, nil}
func main() { result, err := divide(10, 0) if err != nil { fmt.Println("Error:", err) return } fmt.Println("Result:", result)}Key point: the if err != nil check is the standard way to handle errors in Go. There are no hidden exceptions that can "pop out" unexpectedly.
2. Creating and Customizing Errors
Go provides several ways to create errors:
errors.New()— a simple text error.fmt.Errorf()— a formatted error (supports%wfor wrapping).- Custom types — for passing additional context.
Custom Error with Additional Data
type DivisionError struct { Dividend float64 Divisor float64 Message string}
func (e *DivisionError) Error() string { return fmt.Sprintf("%s: %f / %f", e.Message, e.Dividend, e.Divisor)}
func safeDivide(a, b float64) (float64, error) { if b == 0 { return 0, &DivisionError{ Dividend: a, Divisor: b, Message: "division by zero", } } return a / b, nil}
func main() { _, err := safeDivide(10, 0) if err != nil { // Type assertion to get additional fields var divErr *DivisionError if errors.As(err, &divErr) { fmt.Printf("Error with %f divided by %f: %s", divErr.Dividend, divErr.Divisor, divErr.Message) } }}Using errors.As() allows you to check if an error is of a specific type and extract data from it. This is the equivalent of instanceof in other languages.
3. Error Handling Patterns: From Simple to Complex
Go has several established patterns for working with errors:
3.1. Sentinel Errors
These are predefined errors that can be compared using ==. Example: io.EOF.
import ( "errors" "fmt")
var ErrNotFound = errors.New("element not found")
func findUser(id int) (string, error) { if id != 1 { return "", ErrNotFound } return "Alice", nil}
func main() { _, err := findUser(2) if errors.Is(err, ErrNotFound) { fmt.Println("User not found, creating a new one...") }}3.2. Error Wrapping
Starting from Go 1.13, you can wrap errors using %w in fmt.Errorf. This preserves the error chain.
func readConfig(path string) error { data, err := os.ReadFile(path) if err != nil { return fmt.Errorf("reading config %s: %w", path, err) } // ... processing data