Advanced Go Error Handling
Table of Contents
- Go Error Handling Philosophy
- The Essence of the error Interface
- Custom Error Types
- fmt.Errorf and %w for Wrapping Errors
- errors.Is and errors.As
- Sentinel Error Pattern
- Error Handling Best Practices
- Error Handling Patterns in Real-World Projects
1. Go Error Handling Philosophy
1.1 Fundamental Differences from try-catch
In languages like Java, Python, and C++, exception handling relies on the try-catch mechanism—exceptions are "thrown" and propagate up the call stack until caught by a catch block. This pattern has several issues:
- The propagation path of exceptions is implicit; callers don't know what exceptions a function might throw.
- Exception control flow is similar to
goto, disrupting normal code logic. - Prone to misuse (using exceptions for flow control).
Go adopts a completely different philosophy: errors are values. Errors are explicitly passed through function return values, and callers must actively check and handle them.
// Java style (implicit exception propagation)
try {
File f = openFile("config.json");
String data = readAll(f);
Config config = parse(data);
} catch (FileNotFoundException e) {
// Handle file not found
} catch (IOException e) {
// Handle IO error
} catch (ParseException e) {
// Handle parsing error
}
// Go style (explicit error handling)
f, err := os.Open("config.json")
if err != nil {
return fmt.Errorf("failed to open config file: %w", err)
}
defer f.Close()
data, err := io.ReadAll(f)
if err != nil {
return fmt.Errorf("failed to read config file: %w", err)
}
var config Config
if err := json.Unmarshal(data, &config); err != nil {
return fmt.Errorf("failed to parse config file: %w", err)
}
1.2 Advantages of Explicit Error Handling
package main
import (
"fmt"
"strconv"
)
// Each potentially failing operation explicitly returns an error
// Callers can clearly see which steps might fail
func parseAndDouble(s string) (int, error) {
// Step 1: Parse string to integer
n, err := strconv.Atoi(s)
if err != nil {
return 0, fmt.Errorf("failed to parse integer: %w", err)
}
// Step 2: Check range
if n < 0 || n > 1000 {
return 0, fmt.Errorf("value %d out of valid range [0, 1000]", n)
}
return n * 2, nil
}
func main() {
// Normal case
result, err := parseAndDouble("42")
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Println("Result:", result) // Result: 84
// Non-numeric
_, err = parseAndDouble("abc")
if err != nil {
fmt.Println("Error:", err) // Error: failed to parse integer: strconv.Atoi: parsing "abc": invalid syntax
}
// Out of range
_, err = parseAndDouble("9999")
if err != nil {
fmt.Println("Error:", err) // Error: value 9999 out of valid range [0, 1000]
}
}
1.3 Go's Error Handling Maxim
Rob Pike pointed out in his "Errors are values" blog post that since errors are ordinary values, programming techniques can be used to simplify error handling, rather than mechanically using if err != nil.
package main
import (
"bufio"
"fmt"
"os"
)
// errWriter encapsulates a writer and its error state
// Once an error occurs, subsequent write operations are automatically skipped
type errWriter struct {
w *bufio.Writer
err error
}
func (ew *errWriter) write(buf []byte) {
if ew.err != nil {
return // Already an error, skip
}
_, ew.err = ew.w.Write(buf)
}
func (ew *errWriter) flush() error {
if ew.err != nil {
return ew.err
}
return ew.w.Flush()
}
func writeReport(f *os.File) error {
bw := bufio.NewWriter(f)
ew := &errWriter{w: bw}
// No need to check for errors every time, check uniformly at the end
ew.write([]byte("=== Report Title ===\n"))
ew.write([]byte("First part content...\n"))
ew.write([]byte("Second part content...\n"))
ew.write([]byte("=== End of Report ===\n"))
return ew.flush()
}
func main() {
f, err := os.Create("/tmp/report.txt")
if err != nil {
fmt.Println("Failed to create file:", err)
return
}
defer f.Close()
if err := writeReport(f); err != nil {
fmt.Println("Failed to write report:", err)
return
}
fmt.Println("Report written successfully")
}
2. The Essence of the error Interface
2.1 error is an Interface
Go's built-in error type is actually a very simple interface:
// Definition in Go standard library
type error interface {
Error() string
}
Any type that implements the Error() string method satisfies the error interface. This design is extremely concise and powerful.
2.2 errors.New and fmt.Errorf in the Standard Library
package main
import (
"errors"
"fmt"
)
func main() {
// errors.New creates a simple error
err1 := errors.New("something went wrong")
fmt.Println(err1) // something went wrong
fmt.Printf("%T\n", err1) // *errors.errorString
// fmt.Errorf creates a formatted error
name := "config.json"
err2 := fmt.Errorf("cannot open file %s", name)
fmt.Println(err2) // cannot open file config.json
// The zero value of error is nil, indicating no error
var err3 error
fmt.Println(err3 == nil) // true
}
2.3 Internal Implementation of errors.New
// The implementation of the standard library errors package is very simple
// This shows its core logic
package main
import "fmt"
// errorString is a simple struct that implements the error interface
type errorString struct {
s string
}
// The Error method makes errorString satisfy the error interface
func (e *errorString) Error() string {
return e.s
}
// New creates a new error, returning a pointer
// Returning a pointer instead of a value ensures that each New call produces a different error instance
func New(text string) error {
return &errorString{text}
}
func main() {
err1 := New("An error occurred")
err2 := New("An error occurred")
// Although the text is the same, they are different error instances (different pointers)
fmt.Println(err1 == err2) // false
fmt.Println(err1) // An error occurred
}
2.4 The nil error Trap
package main
import "fmt"
// MyError custom error type
type MyError struct {
Code int
Message string
}
func (e *MyError) Error() string {
return fmt.Sprintf("Error code %d: %s", e.Code, e.Message)
}
func doSomething(fail bool) error {
// Note: A pointer to a concrete type is declared here
var err *MyError
if fail {
err = &MyError{Code: 404, Message: "Not Found"}
}
// Dangerous! Even if err is a nil pointer, the error interface value is not nil after return
return err
}
func doSomethingCorrect(fail bool) error {
if fail {
return &MyError{Code: 404, Message: "Not Found"}
}
// Directly return nil, without going through a concrete type variable
return nil
}
func main() {
// Incorrect example: Even if there's no failure, err is not equal to nil!
err := doSomething(false)
if err != nil {
// Will enter here! Because the error interface internally holds (*MyError, nil)
// An interface value is only nil if both its type and value are nil
fmt.Println("Incorrect judgment - entered the error branch")
fmt.Printf("err Type: %T, Value: %v\n", err, err)
}
// Correct example
err2 := doSomethingCorrect(false)
if err2 == nil {
fmt.Println("Correct: no error") // Correct: no error
}
}
Key Lesson: Never return a nil pointer of a concrete type as an error interface value. If there is no error, simply
return nil.
3. Custom Error Types
3.1 Simple Custom Errors
package main
import "fmt"
// ValidationError validation error, contains field name and reason
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("Validation failed [%s]: %s", e.Field, e.Message)
}
// NotFoundError resource not found error
type NotFoundError struct {
Resource string
ID string
}
func (e *NotFoundError) Error() string {
return fmt.Sprintf("%s (ID: %s) does not exist", e.Resource, e.ID)
}
func validateAge(age int) error {
if age < 0 {
return &ValidationError{
Field: "age",
Message: "age cannot be negative",
}
}
if age > 150 {
return &ValidationError{
Field: "age",
Message: "age cannot exceed 150",
}
}
return nil
}
func findUser(id string) (string, error) {
// Simulate finding a user
users := map[string]string{
"1": "Alice",
"2": "Bob",
}
name, ok := users[id]
if !ok {
return "", &NotFoundError{
Resource: "User",
ID: id,
}
}
return name, nil
}
func main() {
// Test validation error
if err := validateAge(-5); err != nil {
fmt.Println(err) // Validation failed [age]: age cannot be negative
// Type assertion to get detailed information
if ve, ok := err.(*ValidationError); ok {
fmt.Printf("Field: %s, Reason: %s\n", ve.Field, ve.Message)
}
}
// Test not found error
_, err := findUser("99")
if err != nil {
fmt.Println(err) // User (ID: 99) does not exist
if nfe, ok := err.(*NotFoundError); ok {
fmt.Printf("Resource Type: %s, ID: %s\n", nfe.Resource, nfe.ID)
}
}
}
3.2 Error Types with Context
package main
import (
"fmt"
"time"
)
// OpError records operation context errors
type OpError struct {
Op string // Operation name
Path string // Resource path
Err error // Underlying error (supports error chaining)
Time time.Time // Time of occurrence
}
func (e *OpError) Error() string {
return fmt.Sprintf("Operation %s on %s failed [%s]: %v",
e.Op, e.Path, e.Time.Format("15:04:05"), e.Err)
}
// Unwrap supports error chain unwrapping
func (e *OpError) Unwrap() error {
return e.Err
}
// PermissionError permission error
type PermissionError struct {
User string
Action string
}
func (e *PermissionError) Error() string {
return fmt.Sprintf("User %s does not have %s permission", e.User, e.Action)
}
func readConfig(user string) error {
// Simulate permission check failure
permErr := &PermissionError{
User: user,
Action: "read",
}
// Wrap as an operation error
return &OpError{
Op: "ReadConfig",
Path: "/etc/app/config.yaml",
Err: permErr,
Time: time.Now(),
}
}
func main() {
err := readConfig("guest")
if err != nil {
fmt.Println(err)
// Operation ReadConfig on /etc/app/config.yaml failed [14:30:05]: User guest does not have read permission
}
}
3.3 Multiple Error Aggregation
package main
import (
"fmt"
"strings"
)
// MultiError aggregates multiple errors
type MultiError struct {
Errors []error
}
func (me *MultiError) Error() string {
if len(me.Errors) == 0 {
return "No errors"
}
msgs := make([]string, len(me.Errors))
for i, err := range me.Errors {
msgs[i] = fmt.Sprintf(" [%d] %s", i+1, err.Error())
}
return fmt.Sprintf("%d errors occurred:\n%s", len(me.Errors), strings.Join(msgs, "\n"))
}
// Add error (ignores nil)
func (me *MultiError) Add(err error) {
if err != nil {
me.Errors = append(me.Errors, err)
}
}
// ErrorOrNil returns nil if no errors
func (me *MultiError) ErrorOrNil() error {
if len(me.Errors) == 0 {
return nil
}
return me
}
// validateUser validates multiple fields simultaneously
func validateUser(name string, age int, email string) error {
me := &MultiError{}
if name == "" {
me.Add(fmt.Errorf("Name cannot be empty"))
}
if age < 0 || age > 150 {
me.Add(fmt.Errorf("Age %d is not within the valid range", age))
}
if !strings.Contains(email, "@") {
me.Add(fmt.Errorf("Email %q format is incorrect", email))
}
return me.ErrorOrNil()
}
func main() {
// Multiple field validation failed
err := validateUser("", -1, "not-an-email")
if err != nil {
fmt.Println(err)
// 3 errors occurred:
// [1] Name cannot be empty
// [2] Age -1 is not within the valid range
// [3] Email "not-an-email" format is incorrect
}
// All validations passed
err = validateUser("Alice", 30, "alice@example.com")
fmt.Println("All passed:", err == nil) // All passed: true
}
4. fmt.Errorf and %w for Wrapping Errors
4.1 Error Wrapping Basics (Go 1.13+)
Go 1.13 introduced the error wrapping mechanism, allowing an error to be wrapped within another error using the %w verb of fmt.Errorf, forming an error chain.
package main
import (
"errors"
"fmt"
"os"
)
func readFile(path string) ([]byte, error) {
data, err := os.ReadFile(path)
if err != nil {
// %w wraps the original error, preserving the error chain
return nil, fmt.Errorf("read file %s: %w", path, err)
}
return data, nil
}
func loadConfig() ([]byte, error) {
data, err := readFile("/etc/app/config.json")
if err != nil {
// Continue wrapping, forming a multi-layer error chain
return nil, fmt.Errorf("load config: %w", err)
}
return data, nil
}
func main() {
_, err := loadConfig()
if err != nil {
fmt.Println("Error message:", err)
// load config: read file /etc/app/config.json: open /etc/app/config.json: no such file or directory
// Can check underlying error with errors.Is
if errors.Is(err, os.ErrNotExist) {
fmt.Println("Root cause: file does not exist")
}
}
}
4.2 Differences between %w and %v
package main
import (
"errors"
"fmt"
"io"
)
func example_w() error {
// %w wraps error -- preserves error chain, can be found with errors.Is/errors.As
return fmt.Errorf("operation failed: %w", io.EOF)
}
func example_v() error {
// %v only formats error text -- loses error chain, cannot be found with errors.Is/errors.As
return fmt.Errorf("operation failed: %v", io.EOF)
}
func main() {
err1 := example_w()
fmt.Println("Using %%w:", errors.Is(err1, io.EOF)) // true -- error chain intact
err2 := example_v()
fmt.Println("Using %%v:", errors.Is(err2, io.EOF)) // false -- error chain broken
}
4.3 Multiple Wrapping (Go 1.20+)
Starting from Go 1.20, fmt.Errorf supports multiple %w, allowing multiple errors to be wrapped simultaneously:
package main
import (
"errors"
"fmt"
"io"
"os"
)
func complexOperation() error {
err1 := os.ErrPermission
err2 := io.ErrUnexpectedEOF
// Wrap two errors simultaneously
return fmt.Errorf("operation failed: %w and %w", err1, err2)
}
func main() {
err := complexOperation()
fmt.Println(err) // operation failed: permission denied and unexpected EOF
// Both underlying errors can be found by errors.Is
fmt.Println("Is it a permission error?", errors.Is(err, os.ErrPermission)) // true
fmt.Println("Is it unexpected EOF?", errors.Is(err, io.ErrUnexpectedEOF)) // true
}
4.4 Manually Implementing Unwrap
package main
import (
"errors"
"fmt"
"io"
)
// QueryError custom type wrapping an underlying error
type QueryError struct {
Query string
Err error
}
func (e *QueryError) Error() string {
return fmt.Sprintf("query %q failed: %v", e.Query, e.Err)
}
// Unwrap allows errors.Is/errors.As to search along the chain
func (e *QueryError) Unwrap() error {
return e.Err
}
// JoinedError wraps multiple underlying errors (Go 1.20 style)
type JoinedError struct {
Message string
Errs []error
}
func (e *JoinedError) Error() string {
return e.Message
}
// Unwrap returns a slice (multi-error unwrapping protocol supported by Go 1.20+)
func (e *JoinedError) Unwrap() []error {
return e.Errs
}
func main() {
// Single error chain
qErr := &QueryError{
Query: "SELECT * FROM users",
Err: io.EOF,
}
fmt.Println(qErr) // query "SELECT * FROM users" failed: EOF
fmt.Println(errors.Is(qErr, io.EOF)) // true
// Multiple error chain
jErr := &JoinedError{
Message: "Multiple operations failed",
Errs: []error{io.EOF, io.ErrClosedPipe},
}
fmt.Println(errors.Is(jErr, io.EOF)) // true
fmt.Println(errors.Is(jErr, io.ErrClosedPipe)) // true
}
5. errors.Is and errors.As
5.1 errors.Is -- Checks if the error chain contains a specific error value
package main
import (
"errors"
"fmt"
"io"
"os"
)
var ErrDatabase = errors.New("Database error")
func queryDB() error {
return fmt.Errorf("execute query: %w", ErrDatabase)
}
func handleRequest() error {
return fmt.Errorf("handle request: %w", queryDB())
}
func main() {
err := handleRequest()
// errors.Is searches layer by layer along the error chain
// err -> "handle request" -> "execute query" -> ErrDatabase
fmt.Println(errors.Is(err, ErrDatabase)) // true
// Common standard library sentinel error judgment
err2 := fmt.Errorf("read finished: %w", io.EOF)
fmt.Println(errors.Is(err2, io.EOF)) // true
// Error not in the error chain
fmt.Println(errors.Is(err, os.ErrNotExist)) // false
// == can only match the same error instance, cannot search along the chain
fmt.Println(err == ErrDatabase) // false (err is wrapped, not equal to the original value)
}
5.2 errors.As -- Extracts a specific type of error from the error chain
package main
import (
"errors"
"fmt"
"net"
)
// APIError custom API error
type APIError struct {
StatusCode int
Message string
}
func (e *APIError) Error() string {
return fmt.Sprintf("API Error %d: %s", e.StatusCode, e.Message)
}
func callAPI() error {
apiErr := &APIError{StatusCode: 403, Message: "Access Denied"}
return fmt.Errorf("call user service: %w", apiErr)
}
func main() {
err := callAPI()
// errors.As finds an error of a matching type in the error chain and assigns it to target
var apiErr *APIError
if errors.As(err, &apiErr) {
fmt.Printf("Status Code: %d, Message: %s\n", apiErr.StatusCode, apiErr.Message)
// Status Code: 403, Message: Access Denied
}
// Practical application in standard library: extracting network error details
_, netErr := net.Dial("tcp", "invalid-host:99999")
if netErr != nil {
var opErr *net.OpError
if errors.As(netErr, &opErr) {
fmt.Printf("Network operation: %s, Address: %v\n", opErr.Op, opErr.Addr)
}
}
}
5.3 Comparison of errors.Is and errors.As
package main
import (
"errors"
"fmt"
"os"
)
func main() {
// Create a file operation error
_, err := os.Open("/这个文件不存在")
if err == nil {
return
}
wrappedErr := fmt.Errorf("initialization failed: %w", err)
// errors.Is: Used to match specific error [values]
// Question: Is this error os.ErrNotExist?
if errors.Is(wrappedErr, os.ErrNotExist) {
fmt.Println("Is: File does not exist") // Match successful
}
// errors.As: Used to match specific error [types] and extract the value of that type
// Question: Is there an error of type *os.PathError in this error chain?
var pathErr *os.PathError
if errors.As(wrappedErr, &pathErr) {
fmt.Printf("As: Path=%s, Operation=%s\n", pathErr.Path, pathErr.Op)
// As: Path=/这个文件不存在, Operation=open
}
// Summary:
// errors.Is(err, target) -> Value comparison, similar to err == target but supports error chaining
// errors.As(err, &target) -> Type matching, similar to type assertion but supports error chaining
}
5.4 Custom Is and As Methods
package main
import (
"errors"
"fmt"
)
// ErrCode error type based on error code
type ErrCode struct {
Code int
Message string
}
func (e *ErrCode) Error() string {
return fmt.Sprintf("[%d] %s", e.Code, e.Message)
}
// Is custom comparison logic: considered a match as long as the error codes are the same
func (e *ErrCode) Is(target error) bool {
t, ok := target.(*ErrCode)
if !ok {
return false
}
return e.Code == t.Code
}
func main() {
err1 := &ErrCode{Code: 404, Message: "User not found"}
err2 := &ErrCode{Code: 404, Message: "Order not found"}
err3 := &ErrCode{Code: 500, Message: "Internal error"}
// Although messages are different, error codes are the same, Is returns true
fmt.Println(errors.Is(err1, err2)) // true
fmt.Println(errors.Is(err1, err3)) // false
// Still effective after wrapping
wrapped := fmt.Errorf("Service layer: %w", err1)
target := &ErrCode{Code: 404} // Only care about the error code
fmt.Println(errors.Is(wrapped, target)) // true
}
6. Sentinel Error Pattern
6.1 What are Sentinel Errors
Sentinel Errors are predefined, package-level error variables used as identifiers for specific conditions. Callers use errors.Is to check if a particular known error has been encountered.
package main
import (
"errors"
"fmt"
"io"
"bufio"
"strings"
)
func main() {
// The most common sentinel error in the standard library: io.EOF
reader := strings.NewReader("Hello")
scanner := bufio.NewScanner(reader)
// io.EOF indicates that data reading is complete, not a true"error"
buf := make([]byte, 10)
for {
n, err := strings.NewReader("Hello").Read(buf)
if errors.Is(err, io.EOF) {
fmt.Printf("Read complete, last read %d bytes\n", n)
break
}
if err != nil {
fmt.Println("Error reading:", err)
break
}
fmt.Printf("Read %d bytes: %s\n", n, buf[:n])
}
_ = scanner
}
6.2 Examples of Sentinel Errors in the Standard Library
package main
import (
"context"
"database/sql"
"errors"
"fmt"
"io"
"os"
"time"
)
func main() {
// 1. io.EOF - End of data stream
fmt.Println("io.EOF:", io.EOF)
// 2. io.ErrUnexpectedEOF - Unexpected termination halfway through reading
fmt.Println("io.ErrUnexpectedEOF:", io.ErrUnexpectedEOF)
// 3. os.ErrNotExist - File does not exist
_, err := os.Stat("/不存在的文件")
if errors.Is(err, os.ErrNotExist) {
fmt.Println("File does not exist")
}
// 4. os.ErrPermission - Insufficient permissions
fmt.Println("os.ErrPermission:", os.ErrPermission)
// 5. sql.ErrNoRows - Query returned no results
// Note: This is not an error, it just indicates no matching rows
fmt.Println("sql.ErrNoRows:", sql.ErrNoRows)
// 6. context.Canceled - Context was canceled
ctx, cancel := context.WithCancel(context.Background())
cancel()
fmt.Println("ctx.Err():", ctx.Err())
fmt.Println("Is it Canceled?", errors.Is(ctx.Err(), context.Canceled))
// 7. context.DeadlineExceeded - Context deadline exceeded
ctx2, cancel2 := context.WithTimeout(context.Background(), 1*time.Nanosecond)
defer cancel2()
time.Sleep(1 * time.Millisecond)
fmt.Println("Is it DeadlineExceeded?", errors.Is(ctx2.Err(), context.DeadlineExceeded))
}
6.3 Custom Sentinel Errors
package main
import (
"errors"
"fmt"
)
// Package-level sentinel errors, created using errors.New
var (
ErrUserNotFound = errors.New("User not found")
ErrUserExists = errors.New("User already exists")
ErrInvalidPassword = errors.New("Invalid password")
ErrAccountLocked = errors.New("Account locked")
)
// UserStore simulates user storage
type UserStore struct {
users map[string]string // username -> password
}
func NewUserStore() *UserStore {
return &UserStore{
users: map[string]string{
"alice": "secret123",
},
}
}
func (s *UserStore) Register(username, password string) error {
if _, exists := s.users[username]; exists {
return fmt.Errorf("register user %s: %w", username, ErrUserExists)
}
s.users[username] = password
return nil
}
func (s *UserStore) Login(username, password string) error {
stored, exists := s.users[username]
if !exists {
return fmt.Errorf("login %s: %w", username, ErrUserNotFound)
}
if stored != password {
return fmt.Errorf("login %s: %w", username, ErrInvalidPassword)
}
return nil
}
func main() {
store := NewUserStore()
// Register an existing user
err := store.Register("alice", "newpass")
if errors.Is(err, ErrUserExists) {
fmt.Println("Registration failed: user already exists")
}
// Log in non-existent user
err = store.Login("bob", "pass")
if errors.Is(err, ErrUserNotFound) {
fmt.Println("Login failed: user not found")
}
// Incorrect password
err = store.Login("alice", "wrong")
if errors.Is(err, ErrInvalidPassword) {
fmt.Println("Login failed: incorrect password")
}
// Use switch to dispatch error handling
err = store.Login("bob", "test")
switch {
case errors.Is(err, ErrUserNotFound):
fmt.Println("-> Suggest registering a new account")
case errors.Is(err, ErrInvalidPassword):
fmt.Println("-> Suggest resetting password")
case errors.Is(err, ErrAccountLocked):
fmt.Println("-> Please contact administrator")
case err != nil:
fmt.Println("-> Unknown error:", err)
default:
fmt.Println("-> Login successful")
}
}
7. Error Handling Best Practices
7.1 When to Use panic
panic is used for unrecoverable program errors, not business logic errors.
package main
import "fmt"
// Scenarios for reasonable use of panic
// 1. Fatal errors during program initialization
func mustConnect(dsn string) {
// If database connection fails, the program cannot run
if dsn == "" {
panic("Database connection string cannot be empty")
}
// ... Connect to database
}
// 2. Must function pattern: convert error to panic
// Standard library has template.Must, regexp.MustCompile, etc.
func MustParseConfig(path string) map[string]string {
config := map[string]string{"key": "value"} // Simulate parsing
if path == "" {
panic("Config file path is empty")
}
return config
}
// 3. Truly impossible errors (program logic bugs)
func divide(a, b int) int {
if b == 0 {
// If the caller guarantees b != 0, a panic here indicates a program bug
panic("Division by zero: this is a programming error")
}
return a / b
}
func main() {
// Must pattern is usually used at the beginning of init or main
config := MustParseConfig("app.yaml")
fmt.Println(config)
}
7.2 recover to Catch panic
package main
import "fmt"
// safeExecute safely executes a function, catching possible panics
func safeExecute(fn func()) (err error) {
defer func() {
if r := recover(); r != nil {
// Convert panic to error
err = fmt.Errorf("Caught panic: %v", r)
}
}()
fn()
return nil
}
func riskyFunction() {
panic("Something went wrong!")
}
func main() {
// Scenario: HTTP middleware prevents a single request's panic from crashing the service
err := safeExecute(riskyFunction)
if err != nil {
fmt.Println("Safely caught:", err)
// Safely caught: Caught panic: Something went wrong!
}
fmt.Println("Program continues to run...")
}
7.3 Golden Rules of Error Handling
package main
import (
"fmt"
"log"
)
// Rule 1: Errors should only be handled once
// Either handle it (log, degrade, return default value), or pass it up
// Do not both log and return an error
// Incorrect example
func badExample() error {
err := fmt.Errorf("something failed")
if err != nil {
log.Println("Error:", err) // Handled once (logged)
return err // Returned again (letting the caller handle it again)
}
return nil
}
// Correct example A: Only pass up (with context)
func goodExampleA() error {
err := fmt.Errorf("something failed")
if err != nil {
return fmt.Errorf("execute operation X: %w", err) // Only pass, do not log
}
return nil
}
// Correct example B: Only handle here
func goodExampleB() int {
err := fmt.Errorf("something failed")
if err != nil {
log.Println("Operation X failed, using default value:", err) // Handle: log + degrade
return 42 // Return default value
}
return 0
}
// Rule 2: Always add context to errors
func fetchUserOrders(userID string) error {
// Bad: return err
// Good: return fmt.Errorf("fetch orders for user %s: %w", userID, err)
return fmt.Errorf("fetch orders for user %s: %w", userID,
fmt.Errorf("database query timeout"))
}
// Rule 3: Handle errors at the top level, only pass them at lower levels
func main() {
// This is the "top level", responsible for final handling
if err := fetchUserOrders("u123"); err != nil {
log.Println("Request processing failed:", err)
// May also need to return HTTP 500, send alerts, etc.
}
_ = badExample()
_ = goodExampleA()
_ = goodExampleB()
}
7.4 Do Not Ignore Errors
package main
import (
"fmt"
"os"
)
func main() {
// Bad: Ignored Close error (write might not have been flushed to disk)
f, _ := os.Create("/tmp/test.txt")
f.Write([]byte("data"))
f.Close() // Close might return an error!
// Correct way
f2, err := os.Create("/tmp/test2.txt")
if err != nil {
fmt.Println("Failed to create file:", err)
return
}
_, err = f2.Write([]byte("data"))
if err != nil {
f2.Close()
fmt.Println("Write failed:", err)
return
}
if err := f2.Close(); err != nil {
fmt.Println("Failed to close file (data might not be fully written):", err)
return
}
// If an error genuinely needs to be ignored, explicitly ignore it with _ and add a comment
_ = os.Remove("/tmp/maybe-not-exists.txt") // Ignore: file might not exist, does not affect logic
}
8. Error Handling Patterns in Real-World Projects
8.1 Layered Error Design
In a layered architecture (Repository -> Service -> Handler), each layer adds its own context:
package main
import (
"errors"
"fmt"
)
// ========== Basic Error Definitions ==========
var (
ErrNotFound = errors.New("Resource not found")
ErrUnauthorized = errors.New("Unauthorized")
ErrInternal = errors.New("Internal error")
)
// ========== Repository Layer ==========
type UserRepo struct{}
func (r *UserRepo) FindByID(id int) (string, error) {
if id == 0 {
return "", fmt.Errorf("UserRepo.FindByID(id=%d): %w", id, ErrNotFound)
}
if id < 0 {
return "", fmt.Errorf("UserRepo.FindByID(id=%d): Database connection failed: %w", id, ErrInternal)
}
return "Alice", nil
}
// ========== Service Layer ==========
type UserService struct {
repo *UserRepo
}
func (s *UserService) GetUserProfile(id int) (string, error) {
name, err := s.repo.FindByID(id)
if err != nil {
return "", fmt.Errorf("UserService.GetUserProfile: %w", err)
}
return fmt.Sprintf("User profile: %s", name), nil
}
// ========== Handler Layer ==========
type UserHandler struct {
service *UserService
}
func (h *UserHandler) HandleGetUser(id int) {
profile, err := h.service.GetUserProfile(id)
if err != nil {
// At the top layer, decide how to respond based on error type
switch {
case errors.Is(err, ErrNotFound):
fmt.Printf("HTTP 404: %v\n", err)
case errors.Is(err, ErrUnauthorized):
fmt.Printf("HTTP 401: %v\n", err)
case errors.Is(err, ErrInternal):
fmt.Printf("HTTP 500: %v\n", err)
default:
fmt.Printf("HTTP 500: Unknown error: %v\n", err)
}
return
}
fmt.Printf("HTTP 200: %s\n", profile)
}
func main() {
handler := &UserHandler{
service: &UserService{
repo: &UserRepo{},
},
}
handler.HandleGetUser(1) // HTTP 200: User profile: Alice
handler.HandleGetUser(0) // HTTP 404: UserService.GetUserProfile: UserRepo.FindByID(id=0): Resource not found
handler.HandleGetUser(-1) // HTTP 500: UserService.GetUserProfile: UserRepo.FindByID(id=-1): Database connection failed: Internal error
}
8.2 Error Code Design Pattern
package main
import (
"errors"
"fmt"
)
// AppError unified application error type
type AppError struct {
Code string // Business error code, e.g., "USER_001"
Message string // User-facing message
Err error // Underlying error (optional)
}
func (e *AppError) Error() string {
if e.Err != nil {
return fmt.Sprintf("[%s] %s: %v", e.Code, e.Message, e.Err)
}
return fmt.Sprintf("[%s] %s", e.Code, e.Message)
}
func (e *AppError) Unwrap() error {
return e.Err
}
// Is allows matching by error code
func (e *AppError) Is(target error) bool {
t, ok := target.(*AppError)
if !ok {
return false
}
return e.Code == t.Code
}
// HTTPStatus returns HTTP status code based on error code prefix
func (e *AppError) HTTPStatus() int {
switch e.Code[:3] {
case "VAL": // Validation error
return 400
case "AUT": // Authentication error
return 401
case "FBD": // Permission error
return 403
case "NTF": // Not found
return 404
default:
return 500
}
}
// Predefined error templates
var (
ErrCodeValidation = &AppError{Code: "VAL_001"}
ErrCodeNotFound = &AppError{Code: "NTF_001"}
ErrCodeAuth = &AppError{Code: "AUT_001"}
ErrCodeInternal = &AppError{Code: "INT_001"}
)
// NewNotFoundError creates a not found error
func NewNotFoundError(resource string, id interface{}) *AppError {
return &AppError{
Code: "NTF_001",
Message: fmt.Sprintf("%s (ID=%v) does not exist", resource, id),
}
}
// NewValidationError creates a validation error
func NewValidationError(field, reason string) *AppError {
return &AppError{
Code: "VAL_001",
Message: fmt.Sprintf("Field %s validation failed: %s", field, reason),
}
}
// NewInternalError creates an internal error (wrapping underlying error)
func NewInternalError(msg string, cause error) *AppError {
return &AppError{
Code: "INT_001",
Message: msg,
Err: cause,
}
}
func processOrder(userID, productID int) error {
if userID <= 0 {
return NewValidationError("userID", "must be greater than 0")
}
if productID == 99 {
return NewNotFoundError("Product", productID)
}
if productID < 0 {
return NewInternalError("Query product failed",
fmt.Errorf("Database timeout"))
}
return nil
}
func handleRequest(userID, productID int) {
err := processOrder(userID, productID)
if err == nil {
fmt.Println("200 OK: Order created successfully")
return
}
// Extract AppError from error
var appErr *AppError
if errors.As(err, &appErr) {
fmt.Printf("HTTP %d | Error Code: %s | %s\n",
appErr.HTTPStatus(), appErr.Code, appErr.Message)
} else {
fmt.Println("HTTP 500 | Unknown error:", err)
}
}
func main() {
handleRequest(1, 1) // 200 OK: Order created successfully
handleRequest(0, 1) // HTTP 400 | Error Code: VAL_001 | Field userID validation failed: must be greater than 0
handleRequest(1, 99) // HTTP 404 | Error Code: NTF_001 | Product (ID=99) does not exist
handleRequest(1, -1) // HTTP 500 | Error Code: INT_001 | Query product failed
// Use errors.Is to match by error code template
err := processOrder(0, 1)
fmt.Println("\nIs it a validation error?", errors.Is(err, ErrCodeValidation)) // true
fmt.Println("Is it not found?", errors.Is(err, ErrCodeNotFound)) // false
}
8.3 Combining defer with Error Handling
package main
import (
"fmt"
"os"
)
// writeToFile demonstrates using defer and named return values to handle close errors
func writeToFile(path, content string) (err error) {
f, err := os.Create(path)
if err != nil {
return fmt.Errorf("create file: %w", err)
}
// Use defer + named return values to handle Close errors
defer func() {
closeErr := f.Close()
if err == nil {
// If write had no error, but Close had an error, return the Close error
err = closeErr
}
// If write already had an error, ignore the Close error (preserve the original error)
}()
_, err = f.WriteString(content)
if err != nil {
return fmt.Errorf("write content: %w", err)
}
return nil
}
// transaction simulates database transaction error handling pattern
func transaction(shouldFail bool) (err error) {
fmt.Println("Start transaction")
defer func() {
if err != nil {
fmt.Println("Rollback transaction")
// rollback()
} else {
fmt.Println("Commit transaction")
// commit()
}
}()
fmt.Println("Execute operation 1")
if shouldFail {
return fmt.Errorf("Operation 1 failed")
}
fmt.Println("Execute operation 2")
return nil
}
func main() {
// Write file
if err := writeToFile("/tmp/test_defer.txt", "Hello, Go!"); err != nil {
fmt.Println("Failed to write file:", err)
} else {
fmt.Println("Write file successful")
}
// Transaction successful
fmt.Println("\n--- Transaction successful ---")
transaction(false)
// Transaction failed
fmt.Println("\n--- Transaction failed ---")
transaction(true)
}
8.4 Using errors.Join (Go 1.20+)
package main
import (
"errors"
"fmt"
"os"
)
// cleanup cleans up multiple resources, collecting all errors
func cleanup(files []*os.File) error {
var errs []error
for _, f := range files {
if err := f.Close(); err != nil {
errs = append(errs, fmt.Errorf("close %s: %w", f.Name(), err))
}
}
// errors.Join merges multiple errors into one
// If errs is empty, returns nil
return errors.Join(errs...)
}
func main() {
// errors.Join basic usage
err := errors.Join(
fmt.Errorf("Error 1: connection timeout"),
fmt.Errorf("Error 2: write failed"),
nil, // nil will be automatically ignored
fmt.Errorf("Error 3: cache miss"),
)
fmt.Println("Merged error:")
fmt.Println(err)
// Error 1: connection timeout
// Error 2: write failed
// Error 3: cache miss
// Errors returned by errors.Join also support errors.Is
sentinel := errors.New("Special error")
joined := errors.Join(
fmt.Errorf("Wrap: %w", sentinel),
fmt.Errorf("Another error"),
)
fmt.Println("\nContains special error?", errors.Is(joined, sentinel)) // true
}
Summary
| Scenario | Recommended Approach |
|---|---|
| Function might fail | Return (result, error) |
| Add context | fmt.Errorf("context: %w", err) |
| Check for specific error value | errors.Is(err, target) |
| Extract specific error type | errors.As(err, &target) |
| Define known error conditions | Package-level sentinel variable var ErrXxx = errors.New(...) |
| Define errors with data | Custom struct implementing error interface |
| Unrecoverable errors | panic (only for program bugs or initialization failures) |
| Merge multiple errors | errors.Join (Go 1.20+) |
| Wrap multiple errors at once | fmt.Errorf("... %w ... %w", err1, err2) (Go 1.20+) |
主题测试文章,只做测试使用。发布者:Walker,转转请注明出处:https://walker-learn.xyz/archives/6719