错误处理
本示例演示 Fox 中的各种错误处理模式,包括简单错误、带状态码的 HTTP 错误和详细的错误响应。
- 简单错误返回
- 带状态码的 HTTP 错误
- 自定义错误定义
- 条件错误处理
- 带附加详情的错误
- Panic 恢复
- 一致的错误响应
package main
import ( "errors" "net/http"
"github.com/fox-gonic/fox" "github.com/fox-gonic/fox/httperrors")
// 定义自定义错误var ( ErrUserNotFound = &httperrors.Error{ HTTPCode: http.StatusNotFound, Code: "USER_NOT_FOUND", Message: "User not found", }
ErrInsufficientBalance = &httperrors.Error{ HTTPCode: http.StatusPaymentRequired, Code: "INSUFFICIENT_BALANCE", Message: "Insufficient account balance", }
ErrDuplicateEmail = &httperrors.Error{ HTTPCode: http.StatusConflict, Code: "DUPLICATE_EMAIL", Message: "Email already exists", }
ErrInvalidCredentials = &httperrors.Error{ HTTPCode: http.StatusUnauthorized, Code: "INVALID_CREDENTIALS", Message: "Invalid username or password", })
type User struct { ID int `json:"id"` Name string `json:"name"` Email string `json:"email"` Balance float64 `json:"balance"`}
func main() { router := fox.New()
// 1. 简单错误 router.GET("/error/simple", func() (string, error) { return "", errors.New("something went wrong") })
// 2. 带状态码的 HTTP 错误 router.GET("/error/http", func() (string, error) { return "", &httperrors.Error{ HTTPCode: http.StatusBadRequest, Code: "INVALID_REQUEST", Err: errors.New("invalid request parameters"), } })
// 3. 条件错误(用户未找到) router.GET("/user/:id", func(ctx *fox.Context) (*User, error) { id := ctx.Param("id")
// 模拟数据库查找 if id != "1" { return nil, ErrUserNotFound }
return &User{ ID: 1, Name: "Alice", Email: "alice@example.com", Balance: 100.50, }, nil })
// 4. 业务逻辑错误(余额不足) type TransferRequest struct { From int `json:"from" binding:"required"` To int `json:"to" binding:"required"` Amount float64 `json:"amount" binding:"required,gt=0"` }
router.POST("/transfer", func(_ *fox.Context, req *TransferRequest) (map[string]any, error) { // 模拟检查余额 currentBalance := 50.0
if req.Amount > currentBalance { return nil, ErrInsufficientBalance }
return map[string]any{ "message": "Transfer successful", "from": req.From, "to": req.To, "amount": req.Amount, }, nil })
// 5. 身份验证错误 type LoginRequest struct { Username string `json:"username" binding:"required"` Password string `json:"password" binding:"required"` }
router.POST("/login", func(_ *fox.Context, req *LoginRequest) (map[string]any, error) { // 模拟凭证检查 if req.Username != "admin" || req.Password != "password" { return nil, ErrInvalidCredentials }
return map[string]any{ "token": "jwt-token-here", }, nil })
// 6. 冲突错误(重复电子邮件) type SignupRequest struct { Email string `json:"email" binding:"required,email"` Password string `json:"password" binding:"required,min=6"` }
router.POST("/signup", func(_ *fox.Context, req *SignupRequest) (map[string]any, error) { // 模拟检查电子邮件是否存在 existingEmails := []string{"alice@example.com", "bob@example.com"}
for _, email := range existingEmails { if email == req.Email { return nil, ErrDuplicateEmail } }
return map[string]any{ "message": "Account created successfully", "email": req.Email, }, nil })
// 7. 权限错误 router.DELETE("/user/:id", func(ctx *fox.Context) (string, error) { id := ctx.Param("id")
// 模拟权限检查 if id == "1" { return "", &httperrors.Error{ HTTPCode: http.StatusForbidden, Code: "CANNOT_DELETE_ADMIN", Err: errors.New("cannot delete admin user"), } }
return "User deleted successfully", nil })
// 8. 带附加详情的错误 router.GET("/detailed-error", func() (string, error) { return "", &httperrors.Error{ HTTPCode: http.StatusUnprocessableEntity, Code: "VALIDATION_FAILED", Message: "Validation failed", Details: map[string]any{ "fields": []map[string]string{ {"field": "email", "error": "invalid format"}, {"field": "age", "error": "must be at least 18"}, }, }, } })
// 9. Panic 恢复(由默认恢复中间件处理) router.GET("/panic", func() string { panic("something went terribly wrong") })
if err := router.Run(":8080"); err != nil { panic(err) }}go run main.gocurl http://localhost:8080/error/simple响应:
{ "error": "something went wrong"}HTTP 错误
Section titled “HTTP 错误”curl http://localhost:8080/error/http响应 (400):
{ "code": "INVALID_REQUEST", "error": "invalid request parameters"}curl http://localhost:8080/user/999响应 (404):
{ "code": "USER_NOT_FOUND", "message": "User not found"}curl -X POST http://localhost:8080/transfer \ -H "Content-Type: application/json" \ -d '{ "from": 1, "to": 2, "amount": 100 }'响应 (402):
{ "code": "INSUFFICIENT_BALANCE", "message": "Insufficient account balance"}curl -X POST http://localhost:8080/login \ -H "Content-Type: application/json" \ -d '{ "username": "wrong", "password": "wrong" }'响应 (401):
{ "code": "INVALID_CREDENTIALS", "message": "Invalid username or password"}重复电子邮件
Section titled “重复电子邮件”curl -X POST http://localhost:8080/signup \ -H "Content-Type: application/json" \ -d '{ "email": "alice@example.com", "password": "password123" }'响应 (409):
{ "code": "DUPLICATE_EMAIL", "message": "Email already exists"}curl http://localhost:8080/detailed-error响应 (422):
{ "code": "VALIDATION_FAILED", "message": "Validation failed", "details": { "fields": [ {"field": "email", "error": "invalid format"}, {"field": "age", "error": "must be at least 18"} ] }}Panic 恢复
Section titled “Panic 恢复”curl http://localhost:8080/panic响应 (500):
{ "error": "Internal Server Error"}1. 简单错误
Section titled “1. 简单错误”返回基本错误消息,状态码为 500:
return "", errors.New("something went wrong")2. 带状态码的 HTTP 错误
Section titled “2. 带状态码的 HTTP 错误”返回带自定义状态码的错误:
return "", &httperrors.Error{ HTTPCode: http.StatusBadRequest, Code: "ERROR_CODE", Err: errors.New("error message"),}3. 带附加详情的 HTTP 错误
Section titled “3. 带附加详情的 HTTP 错误”返回带额外信息的错误:
return "", &httperrors.Error{ HTTPCode: http.StatusUnprocessableEntity, Code: "VALIDATION_FAILED", Message: "Validation failed", Details: map[string]any{ "fields": []string{"email", "password"}, },}HTTP 状态码
Section titled “HTTP 状态码”不同错误场景的常用状态码:
400 Bad Request- 无效的请求参数401 Unauthorized- 需要身份验证或身份验证失败403 Forbidden- 权限被拒绝404 Not Found- 资源未找到409 Conflict- 资源冲突(例如,重复电子邮件)422 Unprocessable Entity- 验证失败429 Too Many Requests- 超过速率限制500 Internal Server Error- 服务器错误
- 使用一致的错误代码: 以 UPPER_SNAKE_CASE 定义错误代码
- 提供用户友好的消息: 不要暴露内部实现细节
- 使用适当的状态码: 匹配 HTTP 语义
- 记录错误: 使用上下文记录错误以便调试
- 避免敏感数据: 永远不要在错误消息中暴露敏感信息
- 尽早处理错误: 一旦发生错误就返回
- 使用自定义错误类型: 为常见场景定义可重用的错误类型
- 添加上下文: 在需要时用额外的上下文包装错误