Error handling is a fundamental aspect of writing robust Go programs. Go encourages explicit error checking rather than exceptions, using multiple return values where one is often an error type. This guide covers best practices, common patterns, and advanced techniques for effective error handling.
Basic Error Handling
In Go, functions that can fail typically return an error as the last return value. Always check for errors immediately after calling such functions.
package main
import (
"fmt"
"os"
)
func main() {
data, err := os.ReadFile("hello.txt")
if err != nil {
fmt.Println("Error reading file:", err)
return
}
fmt.Println("File content:", string(data))
}
Key Points:
- Use
os.ReadFileinstead of the deprecatedioutil.ReadFile. - Check
err != nilright after the function call. - Handle errors gracefully, e.g., by logging or returning early.
Don’t Use Panic for Expected Errors
Panic is for unrecoverable errors, like programming bugs. For expected errors (e.g., file not found), use error returns instead.
// Bad: Using panic for expected errors
data, err := os.ReadFile("hello.txt")
if err != nil {
panic(err) // Avoid this for recoverable errors
}
// Good: Handle the error
if err != nil {
fmt.Println("Error:", err)
// Continue or return
}
When to Use Panic:
- During initialization if the program cannot continue.
- In library code for truly unexpected conditions.
Wrapping Errors
Go 1.13 introduced error wrapping with %w verb in fmt.Errorf to preserve the original error for inspection.
import (
"fmt"
"errors"
)
func Open(path string) (*DB, error) {
db, err := bolt.Open(path, 0600, &bolt.Options{Timeout: 1 * time.Second})
if err != nil {
return nil, fmt.Errorf("opening bolt db: %w", err)
}
return &DB{db}, nil
}
Benefits:
- Use
errors.Is()to check if an error matches a specific type. - Use
errors.As()to extract the underlying error.
Custom Errors
Create custom error types for more context.
type MyError struct {
Msg string
Code int
}
func (e *MyError) Error() string {
return fmt.Sprintf("error %d: %s", e.Code, e.Msg)
}
func doSomething() error {
return &MyError{"something went wrong", 42}
}
Defer for Cleanup
Use defer to ensure cleanup happens even if an error occurs.
func processFile(filename string) error {
file, err := os.Open(filename)
if err != nil {
return err
}
defer file.Close() // Ensures file is closed
// Process file...
return nil
}
Best Practices
- Fail Fast: Check errors early and return immediately.
- Don’t Ignore Errors: Always handle or propagate errors.
- Consistent Error Messages: Use clear, actionable error messages.
- Logging: Use structured logging for errors in production.
- Testing: Write tests that cover error paths.
Conclusion
Effective error handling makes Go programs more reliable and maintainable. By following these patterns, you can write code that gracefully handles failures and provides useful debugging information.