错误处理的高级技巧:从基础到企业级
Go 的错误处理以其简洁性著称,但简洁不等于简单。在实际项目中,我们需要处理复杂的错误场景:错误包装、错误链、错误分类、错误监控等。
本文将带你从基础的错误处理进阶到企业级的错误处理策略。
回顾基础
基本的错误处理
package main
import (
"errors"
"fmt"
)
func divide(a, b int) (int, error) {
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil
}
func main() {
result, err := divide(10, 0)
if err != nil {
fmt.Printf("Error: %v\n", err)
return
}
fmt.Printf("Result: %d\n", result)
}
这种基础模式适用于简单场景,但在复杂应用中远远不够。
错误包装与上下文
使用 fmt.Errorf 添加上下文
package main
import (
"fmt"
"os"
)
func readFile(path string) ([]byte, error) {
data, err := os.ReadFile(path)
if err != nil {
// ❌ 不好:丢失了原始错误的上下文
return nil, err
}
return data, nil
}
func readFileBetter(path string) ([]byte, error) {
data, err := os.ReadFile(path)
if err != nil {
// ✅ 好:添加上下文信息
return nil, fmt.Errorf("read file %s: %w", path, err)
}
return data, nil
}
func main() {
_, err := readFileBetter("/nonexistent.txt")
if err != nil {
fmt.Println(err)
// 输出:read file /nonexistent.txt: open /nonexistent.txt: no such file or directory
}
}
多层包装
package main
import (
"fmt"
"os"
)
func loadConfig(path string) ([]byte, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("load config: %w", err)
}
return data, nil
}
func initApp(configPath string) error {
_, err := loadConfig(configPath)
if err != nil {
return fmt.Errorf("init app: %w", err)
}
return nil
}
func main() {
err := initApp("/nonexistent.conf")
if err != nil {
fmt.Println(err)
// 输出:init app: load config: open /nonexistent.conf: no such file or directory
}
}
错误链与错误检查
使用 errors.Is 检查错误
package main
import (
"errors"
"fmt"
"os"
)
func main() {
_, err := os.Open("/nonexistent.txt")
if err != nil {
// ✅ 使用 errors.Is 检查特定错误
if errors.Is(err, os.ErrNotExist) {
fmt.Println("File does not exist")
} else if errors.Is(err, os.ErrPermission) {
fmt.Println("Permission denied")
} else {
fmt.Printf("Other error: %v\n", err)
}
}
}
使用 errors.As 提取错误类型
package main
import (
"errors"
"fmt"
"net"
)
func connectToServer(addr string) error {
_, err := net.Dial("tcp", addr)
if err != nil {
return fmt.Errorf("connect to %s: %w", addr, err)
}
return nil
}
func main() {
err := connectToServer("invalid-address:9999")
if err != nil {
// ✅ 使用 errors.As 提取特定类型的错误
var netErr net.Error
if errors.As(err, &netErr) {
fmt.Printf("Network error: timeout=%v, temporary=%v\n",
netErr.Timeout(), netErr.Temporary())
}
}
}
自定义错误类型
基本的自定义错误
package main
import (
"fmt"
)
// ValidationError 验证错误
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("validation failed: %s - %s", e.Field, e.Message)
}
func validateAge(age int) error {
if age < 0 || age > 150 {
return &ValidationError{
Field: "age",
Message: "must be between 0 and 150",
}
}
return nil
}
func main() {
err := validateAge(200)
if err != nil {
fmt.Println(err)
// 输出:validation failed: age - must be between 0 and 150
}
}
带错误码的错误类型
package main
import (
"fmt"
)
type ErrorCode int
const (
ErrCodeNotFound ErrorCode = iota + 1
ErrCodeInvalidInput
ErrCodeUnauthorized
ErrCodeInternal
)
type AppError struct {
Code ErrorCode
Message string
Err error
}
func (e *AppError) Error() string {
if e.Err != nil {
return fmt.Sprintf("[%d] %s: %v", e.Code, e.Message, e.Err)
}
return fmt.Sprintf("[%d] %s", e.Code, e.Message)
}
func (e *AppError) Unwrap() error {
return e.Err
}
// 构造函数
func NewNotFoundError(resource string) *AppError {
return &AppError{
Code: ErrCodeNotFound,
Message: fmt.Sprintf("%s not found", resource),
}
}
func NewInvalidInputError(field, reason string) *AppError {
return &AppError{
Code: ErrCodeInvalidInput,
Message: fmt.Sprintf("invalid %s: %s", field, reason),
}
}
func NewInternalError(err error) *AppError {
return &AppError{
Code: ErrCodeInternal,
Message: "internal server error",
Err: err,
}
}
func main() {
err := NewNotFoundError("user")
fmt.Println(err) // [1] user not found
err2 := NewInvalidInputError("email", "invalid format")
fmt.Println(err2) // [2] invalid email: invalid format
}
错误集合
package main
import (
"fmt"
"strings"
)
type MultiError struct {
Errors []error
}
func (m *MultiError) Error() string {
if len(m.Errors) == 0 {
return ""
}
var msgs []string
for _, err := range m.Errors {
msgs = append(msgs, err.Error())
}
return fmt.Sprintf("multiple errors: %s", strings.Join(msgs, "; "))
}
func (m *MultiError) Add(err error) {
if err != nil {
m.Errors = append(m.Errors, err)
}
}
func (m *MultiError) HasErrors() bool {
return len(m.Errors) > 0
}
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("%s: %s", e.Field, e.Message)
}
func validateUser(name, email string, age int) error {
var errs MultiError
if name == "" {
errs.Add(&ValidationError{"name", "cannot be empty"})
}
if email == "" {
errs.Add(&ValidationError{"email", "cannot be empty"})
}
if age < 0 || age > 150 {
errs.Add(&ValidationError{"age", "must be between 0 and 150"})
}
if errs.HasErrors() {
return &errs
}
return nil
}
func main() {
err := validateUser("", "", -1)
if err != nil {
fmt.Println(err)
// 输出:multiple errors: name: cannot be empty; email: cannot be empty; age: must be between 0 and 150
}
}
错误处理策略
策略 1:Fail Fast(快速失败)
package main
import (
"errors"
"fmt"
)
func processOrder(orderID string, amount float64) error {
// ✅ 快速验证输入
if orderID == "" {
return errors.New("orderID cannot be empty")
}
if amount <= 0 {
return errors.New("amount must be positive")
}
// 业务逻辑
fmt.Printf("Processing order %s for $%.2f\n", orderID, amount)
return nil
}
func main() {
err := processOrder("", 100)
if err != nil {
fmt.Println(err)
return
}
}
策略 2:错误转换
package main
import (
"database/sql"
"errors"
"fmt"
)
type Repository struct {
db *sql.DB
}
type User struct {
ID int
Name string
}
var ErrUserNotFound = errors.New("user not found")
func (r *Repository) GetUser(id int) (*User, error) {
var user User
err := r.db.QueryRow("SELECT id, name FROM users WHERE id = ?", id).
Scan(&user.ID, &user.Name)
if err == sql.ErrNoRows {
// ✅ 将数据库错误转换为业务错误
return nil, ErrUserNotFound
}
if err != nil {
return nil, fmt.Errorf("query user: %w", err)
}
return &user, nil
}
策略 3:错误聚合与重试
package main
import (
"context"
"errors"
"fmt"
"math/rand"
"time"
)
type RetryConfig struct {
MaxAttempts int
Delay time.Duration
}
func withRetry(ctx context.Context, config RetryConfig, fn func() error) error {
var lastErr error
for attempt := 1; attempt <= config.MaxAttempts; attempt++ {
err := fn()
if err == nil {
return nil
}
lastErr = err
fmt.Printf("Attempt %d failed: %v\n", attempt, err)
if attempt < config.MaxAttempts {
select {
case <-ctx.Done():
return ctx.Err()
case <-time.After(config.Delay):
// 继续重试
}
}
}
return fmt.Errorf("all %d attempts failed, last error: %w",
config.MaxAttempts, lastErr)
}
func unstableOperation() error {
// 模拟不稳定的操作
if rand.Float32() < 0.7 {
return errors.New("temporary failure")
}
return nil
}
func main() {
ctx := context.Background()
config := RetryConfig{
MaxAttempts: 3,
Delay: 1 * time.Second,
}
err := withRetry(ctx, config, unstableOperation)
if err != nil {
fmt.Printf("Operation failed: %v\n", err)
} else {
fmt.Println("Operation succeeded")
}
}
企业级错误处理
分层错误处理架构
package main
import (
"encoding/json"
"fmt"
"net/http"
)
// 1. 领域层错误
type DomainError struct {
Code string
Message string
}
func (e *DomainError) Error() string {
return fmt.Sprintf("[%s] %s", e.Code, e.Message)
}
var (
ErrUserNotFound = &DomainError{"USER_NOT_FOUND", "User not found"}
ErrInvalidPassword = &DomainError{"INVALID_PASSWORD", "Invalid password"}
)
// 2. 应用层错误
type AppError struct {
DomainErr *DomainError
Context map[string]interface{}
Err error
}
func (e *AppError) Error() string {
return fmt.Sprintf("%v (context: %v)", e.DomainErr, e.Context)
}
func (e *AppError) Unwrap() error {
return e.Err
}
// 3. 传输层错误
type HTTPError struct {
StatusCode int
Code string
Message string
Details interface{}
}
func (e *HTTPError) Error() string {
return fmt.Sprintf("HTTP %d: [%s] %s", e.StatusCode, e.Code, e.Message)
}
// 错误转换器
func toHTTPError(err error) *HTTPError {
var appErr *AppError
if errors.As(err, &appErr) {
switch appErr.DomainErr.Code {
case "USER_NOT_FOUND":
return &HTTPError{
StatusCode: http.StatusNotFound,
Code: appErr.DomainErr.Code,
Message: appErr.DomainErr.Message,
Details: appErr.Context,
}
case "INVALID_PASSWORD":
return &HTTPError{
StatusCode: http.StatusUnauthorized,
Code: appErr.DomainErr.Code,
Message: appErr.DomainErr.Message,
}
}
}
// 默认:内部错误
return &HTTPError{
StatusCode: http.StatusInternalServerError,
Code: "INTERNAL_ERROR",
Message: "An unexpected error occurred",
}
}
// HTTP Handler
func handleError(w http.ResponseWriter, err error) {
httpErr := toHTTPError(err)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(httpErr.StatusCode)
json.NewEncoder(w).Encode(map[string]interface{}{
"error": map[string]interface{}{
"code": httpErr.Code,
"message": httpErr.Message,
"details": httpErr.Details,
},
})
}
错误监控与告警
package main
import (
"fmt"
"log"
"os"
)
// 错误分类
type ErrorSeverity int
const (
SeverityInfo ErrorSeverity = iota
SeverityWarning
SeverityError
SeverityCritical
)
type MonitoredError struct {
Err error
Severity ErrorSeverity
Context map[string]interface{}
}
// 错误监控器
type ErrorMonitor struct {
logger *log.Logger
}
func NewErrorMonitor() *ErrorMonitor {
return &ErrorMonitor{
logger: log.New(os.Stdout, "[ERROR] ", log.LstdFlags),
}
}
func (m *ErrorMonitor) Report(err *MonitoredError) {
// 记录日志
m.logger.Printf("[%v] %v (context: %v)",
err.Severity, err.Err, err.Context)
// 根据严重程度发送告警
switch err.Severity {
case SeverityCritical:
m.sendAlert(err)
case SeverityError:
m.notifyTeam(err)
}
}
func (m *ErrorMonitor) sendAlert(err *MonitoredError) {
fmt.Printf("🚨 CRITICAL ALERT: %v\n", err.Err)
// 发送短信、邮件、Slack 等
}
func (m *ErrorMonitor) notifyTeam(err *MonitoredError) {
fmt.Printf("⚠️ Team notification: %v\n", err.Err)
// 发送 Slack 消息等
}
// 使用示例
func processPayment(amount float64) error {
if amount < 0 {
return &MonitoredError{
Err: fmt.Errorf("invalid amount: %.2f", amount),
Severity: SeverityWarning,
Context: map[string]interface{}{
"amount": amount,
},
}
}
// 模拟支付失败
if amount > 10000 {
return &MonitoredError{
Err: fmt.Errorf("payment gateway timeout"),
Severity: SeverityCritical,
Context: map[string]interface{}{
"amount": amount,
"transaction": "TX-123",
},
}
}
return nil
}
func main() {
monitor := NewErrorMonitor()
err := processPayment(15000)
if err != nil {
if monErr, ok := err.(*MonitoredError); ok {
monitor.Report(monErr)
}
}
}
错误处理中间件
package main
import (
"context"
"encoding/json"
"fmt"
"net/http"
"runtime/debug"
"time"
)
type contextKey string
const requestIDKey contextKey = "requestID"
// 错误处理中间件
func errorHandlingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 恢复 panic
defer func() {
if r := recover(); r != nil {
// 记录堆栈
stack := debug.Stack()
fmt.Printf("Panic: %v\n%s\n", r, stack)
// 返回 500 错误
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
}
}()
// 添加超时
ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second)
defer cancel()
// 添加 request ID
requestID := r.Header.Get("X-Request-ID")
if requestID == "" {
requestID = generateRequestID()
}
ctx = context.WithValue(ctx, requestIDKey, requestID)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
func generateRequestID() string {
return fmt.Sprintf("req-%d", time.Now().UnixNano())
}
// 错误响应中间件
func errorResponseMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 包装 ResponseWriter 以捕获错误
wrapped := &responseWriter{
ResponseWriter: w,
statusCode: http.StatusOK,
}
next.ServeHTTP(wrapped, r)
// 如果有错误,记录并返回标准格式
if wrapped.statusCode >= 400 {
requestID, _ := r.Context().Value(requestIDKey).(string)
fmt.Printf("[%s] HTTP %d: %s %s\n",
requestID, wrapped.statusCode, r.Method, r.URL.Path)
}
})
}
type responseWriter struct {
http.ResponseWriter
statusCode int
}
func (rw *responseWriter) WriteHeader(code int) {
rw.statusCode = code
rw.ResponseWriter.WriteHeader(code)
}
// 示例 Handler
func userHandler(w http.ResponseWriter, r *http.Request) {
userID := r.URL.Query().Get("id")
if userID == "" {
http.Error(w, "missing user ID", http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{
"id": userID,
"name": "John Doe",
})
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/user", userHandler)
// 应用中间件
handler := errorHandlingMiddleware(
errorResponseMiddleware(mux),
)
http.ListenAndServe(":8080", handler)
}
错误处理最佳实践
1. 错误信息要清晰
// ❌ 不好:信息不明确
return errors.New("error")
// ✅ 好:信息清晰
return fmt.Errorf("failed to connect to database at %s: %w", dbHost, err)
2. 不要忽略错误
// ❌ 不好:忽略错误
result, _ := doSomething()
// ✅ 好:处理错误
result, err := doSomething()
if err != nil {
return fmt.Errorf("do something: %w", err)
}
3. 使用 sentinel errors
// ✅ 定义 sentinel errors
var (
ErrNotFound = errors.New("not found")
ErrUnauthorized = errors.New("unauthorized")
ErrForbidden = errors.New("forbidden")
)
// 使用时检查
if errors.Is(err, ErrNotFound) {
// 处理 not found
}
4. 错误处理要幂等
// ✅ 幂等的错误处理
func cleanup() error {
// 多次调用都是安全的
os.Remove(tempFile)
return nil
}
5. 记录错误上下文
// ✅ 记录完整的上下文
if err != nil {
log.Printf("failed to process order: orderID=%s, userID=%s, err=%v",
orderID, userID, err)
return fmt.Errorf("process order %s: %w", orderID, err)
}
总结
错误处理是 Go 编程的核心技能:
基础技巧:
- 使用
fmt.Errorf添加上下文 - 使用
%w包装错误 - 使用
errors.Is和errors.As检查错误
高级技巧:
- 自定义错误类型
- 错误集合
- 错误转换
- 错误聚合与重试
企业级策略:
- 分层错误处理架构
- 错误监控与告警
- 错误处理中间件
- 统一的错误响应格式
最佳实践:
- 错误信息要清晰
- 不要忽略错误
- 使用 sentinel errors
- 错误处理要幂等
- 记录错误上下文
记住:错误不是异常,而是值。正确处理错误,让你的程序更加健壮和可靠。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。