Go Learning Path - Module 9: Error Handling and Defer/Panic/Recover
go教程目录
Module 1: Hello World & Basic Concepts
Module 2: Variables, Data Types, and Constants
Module 2: Variables, Data Types, and Constants
Module 4: Control Structures (if/else, loops)
Module 5: Arrays, Slices, and Maps Arrays
Module 6: Structs and Interfaces
Module 7: Pointers and Memory Management
Module 8: Concurrency with Goroutines and Channels
Module 9: Error Handling and Defer/Panic/Recover
Module 10: Advanced Topics - Testing and Standard Library
Proper error handling is essential in Go. Go's approach to error handling is explicit and idiomatic, using multiple return values where errors are returned as a second (or subsequent) value. Additionally, Go provides defer, panic, and recover for more complex error scenarios.
Error Handling
In Go, functions often return an error value as their last return value. The caller should check this error:
package main
import (
"errors"
"fmt"
"strconv"
)
// Function that returns an error
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil
}
func main() {
result, err := divide(10, 3)
if err != nil {
fmt.Printf("Error: %v\n", err)
} else {
fmt.Printf("Result: %.2f\n", result)
}
// This will cause an error
result, err = divide(10, 0)
if err != nil {
fmt.Printf("Error: %v\n", err) // Error: division by zero
} else {
fmt.Printf("Result: %.2f\n", result)
}
}
// More detailed error with custom types
type DivideError struct {
dividend float64
divisor float64
}
func (e DivideError) Error() string {
return fmt.Sprintf("cannot divide %.2f by %.2f", e.dividend, e.divisor)
}
func divideDetailed(a, b float64) (float64, error) {
if b == 0 {
return 0, DivideError{dividend: a, divisor: b}
}
return a / b, nil
}
func mainDetailed() {
result, err := divideDetailed(10, 0)
if err != nil {
fmt.Printf("Detailed error: %v\n", err)
// Type assertion to access custom error fields
if divErr, ok := err.(DivideError); ok {
fmt.Printf("Tried to divide %.2f by %.2f\n", divErr.dividend, divErr.divisor)
}
} else {
fmt.Printf("Result: %.2f\n", result)
}
}
Using fmt.Errorf for formatted errors
package main
import (
"errors"
"fmt"
)
func validateAge(age int) error {
if age < 0 {
return fmt.Errorf("age cannot be negative: %d", age)
}
if age > 150 {
return fmt.Errorf("age seems unrealistic: %d", age)
}
return nil
}
func processUser(name string, age int) error {
if err := validateAge(age); err != nil {
return fmt.Errorf("invalid user data for %s: %w", name, err)
}
fmt.Printf("Processing user: %s, age: %d\n", name, age)
return nil
}
func main() {
err := processUser("Alice", -5)
if err != nil {
fmt.Printf("Error: %v\n", err)
}
}
The Defer Statement
The defer statement postpones the execution of a function call until the surrounding function returns:
package main
import "fmt"
func main() {
fmt.Println("First")
defer fmt.Println("This will be printed second-to-last")
defer fmt.Println("This will be printed last")
fmt.Println("Second")
// Defer statements are executed in LIFO (Last In, First Out) order
}
// Practical example: ensuring resources are closed
func processFile() error {
fmt.Println("Opening file...")
defer func() {
fmt.Println("Closing file...")
// In real code: f.Close()
}()
fmt.Println("Processing file content...")
// If there's an error, the deferred function still runs
if true { // Simulate some condition
return fmt.Errorf("something went wrong")
}
fmt.Println("File processed successfully")
return nil
}
func mainDefer() {
err := processFile()
if err != nil {
fmt.Printf("Error occurred: %v\n", err)
}
}
Multiple Defers
package main
import "fmt"
func multipleDefers() {
for i := 0; i < 3; i++ {
defer fmt.Printf("defer %d\n", i)
}
fmt.Println("End of function")
}
func main() {
multipleDefers()
fmt.Println("After function call")
}
// Output:
// End of function
// defer 2
// defer 1
// defer 0
// After function call
Defer with Arguments Evaluation
package main
import "fmt"
func deferArguments() {
x := 1
// The value of x is evaluated immediately when defer is called
defer fmt.Printf("defer: x = %d\n", x)
x = 2
fmt.Printf("before return: x = %d\n", x)
}
func main() {
deferArguments()
}
// Output:
// before return: x = 2
// defer: x = 1
Panic and Recover
panic causes a run-time error that stops normal execution and begins panicking. recover can stop the panic and restore normal execution:
package main
import "fmt"
func mightPanic(shouldPanic bool) {
if shouldPanic {
fmt.Println("About to panic...")
panic("Something went terribly wrong!")
}
fmt.Println("No panic here")
}
func handlePanic() {
defer func() {
if r := recover(); r != nil {
fmt.Printf("Recovered from panic: %v\n", r)
}
}()
// This won't panic
mightPanic(false)
// This will panic but be recovered
mightPanic(true)
fmt.Println("After potential panic")
}
func main() {
handlePanic()
fmt.Println("Continued execution after panic recovery")
}
When to Use Panic
Use panic for truly exceptional situations, not for normal error conditions:
package main
import "fmt"
func safeAccess(slice []int, index int) int {
if index < 0 || index >= len(slice) {
// This is an exceptional situation - don't use panic for expected errors
panic(fmt.Sprintf("index %d out of bounds for slice of length %d", index, len(slice)))
}
return slice[index]
}
// Better approach for expected errors
func safeAccessWithError(slice []int, index int) (int, error) {
if index < 0 || index >= len(slice) {
return 0, fmt.Errorf("index %d out of bounds for slice of length %d", index, len(slice))
}
return slice[index], nil
}
func main() {
slice := []int{1, 2, 3, 4, 5}
// Safe access
fmt.Printf("Element at index 2: %d\n", safeAccess(slice, 2))
// Error handling approach
if val, err := safeAccessWithError(slice, 10); err != nil {
fmt.Printf("Error: %v\n", err)
} else {
fmt.Printf("Value: %d\n", val)
}
}
Comprehensive Error Handling Example
package main
import (
"errors"
"fmt"
"io"
"os"
)
// Custom error types
var (
ErrNotFound = errors.New("item not found")
ErrPermission = errors.New("permission denied")
)
// A function that returns different types of errors
func processFile(filename string) error {
file, err := os.Open(filename)
if err != nil {
if os.IsNotExist(err) {
return fmt.Errorf("file %s: %w", filename, ErrNotFound)
}
if os.IsPermission(err) {
return fmt.Errorf("file %s: %w", filename, ErrPermission)
}
return fmt.Errorf("could not open file %s: %w", filename, err)
}
// Use defer to ensure the file is closed
defer func() {
if closeErr := file.Close(); closeErr != nil {
fmt.Printf("Error closing file: %v\n", closeErr)
}
}()
// Simulate some processing that might fail
data := make([]byte, 100)
_, err = file.Read(data)
if err != nil && err != io.EOF {
return fmt.Errorf("could not read file %s: %w", filename, err)
}
fmt.Printf("Successfully processed file: %s\n", filename)
return nil
}
func main() {
// Test with non-existent file
err := processFile("nonexistent.txt")
if err != nil {
// Check for specific error types
if errors.Is(err, ErrNotFound) {
fmt.Printf("File not found error: %v\n", err)
} else {
fmt.Printf("Other error: %v\n", err)
}
}
}
Advanced Defer Usage
package main
import "fmt"
// Defer in loops - be careful!
func badLoopDefer() {
fmt.Println("Bad example - defer in loop:")
for i := 0; i < 3; i++ {
defer fmt.Printf("defer in loop: %d\n", i)
}
fmt.Println("Loop end")
}
func goodLoopDefer() {
fmt.Println("\nGood example - defer in function called from loop:")
for i := 0; i < 3; i++ {
func(index int) {
defer fmt.Printf("defer in function: %d\n", index)
fmt.Printf("processing: %d\n", index)
}(i)
}
fmt.Println("Function calls end")
}
func main() {
badLoopDefer()
goodLoopDefer()
}
Panic/Recover in HTTP Handlers
package main
import (
"fmt"
"log"
"net/http"
)
// Middleware to recover from panics in HTTP handlers
func recoverMiddleware(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
defer func() {
if r := recover(); r != nil {
log.Printf("Recovered from panic: %v", r)
http.Error(w, "Internal server error", http.StatusInternalServerError)
}
}()
next.ServeHTTP(w, r)
}
}
func panicHandler(w http.ResponseWriter, r *http.Request) {
panic("Something went wrong in the handler!")
}
func normalHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, World!")
}
func main() {
// In a real application, you'd run this
// http.HandleFunc("/panic", recoverMiddleware(panicHandler))
// http.HandleFunc("/normal", recoverMiddleware(normalHandler))
// log.Fatal(http.ListenAndServe(":8080", nil))
}
Best Practices
1. Always Check Errors
// Good
data, err := someFunction()
if err != nil {
// handle error appropriately
return err
}
// use data safely
// Bad
data, _ := someFunction() // ignoring error
// use data without knowing if it's valid
2. Use Defer for Cleanup
// Good
func processFile(filename string) error {
file, err := os.Open(filename)
if err != nil {
return err
}
defer file.Close() // Guaranteed to run
// Process file...
return nil
}
3. Be Specific with Errors
// Good - more informative error
func validateEmail(email string) error {
if len(email) == 0 {
return errors.New("email cannot be empty")
}
if !strings.Contains(email, "@") {
return errors.New("invalid email format: missing @ symbol")
}
return nil
}
4. Don't Panic for Expected Errors
// Good
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil
}
// Avoid this for expected conditions
func dividePanic(a, b float64) float64 {
if b == 0 {
panic("division by zero") // Not good for expected errors
}
return a / b
}
Exercises
-
Write a function that reads a file, processes its content, and handles all possible errors appropriately using defer for cleanup.
-
Create a function that demonstrates the difference between panic/recover and regular error handling.
-
Implement a simple web server with proper panic recovery middleware.
-
Write a function that opens multiple resources, ensures all are closed using defer, and handles errors properly.
-
Create a custom error type with additional fields and demonstrate error wrapping using fmt.Errorf.
Next: Module 10: Advanced Topics - Testing and Standard Library