跳转至

6.7 响应处理与错误管理

作为有三十年Go语言开发经验的老师,我很高兴为你讲解响应处理与错误管理这一重要主题。良好的响应处理和错误管理机制是构建健壮、可维护Web应用的基础。

1. 响应格式标准化

在Web开发中,保持响应格式的一致性至关重要。这能让前端开发者更容易处理响应,并提供更好的用户体验。

通用响应结构

我们使用统一的JSON响应格式:

{
  "code": int,    // 状态码
  "msg": string,  // 消息描述
  "data": any     // 业务数据
}

响应工具函数封装

下面是完整的响应处理工具实现:

package main

import (
    "net/http"

    "github.com/gin-gonic/gin"
)

// Response 通用响应结构
type Response struct {
    Code int         `json:"code"`
    Msg  string      `json:"msg"`
    Data interface{} `json:"data,omitempty"`
}

// Success 成功响应
func Success(c *gin.Context, data interface{}) {
    c.JSON(http.StatusOK, Response{
        Code: 0,
        Msg:  "success",
        Data: data,
    })
}

// Error 错误响应
func Error(c *gin.Context, code int, msg string) {
    c.JSON(http.StatusOK, Response{
        Code: code,
        Msg:  msg,
        Data: nil,
    })
}

// ErrorWithData 带数据的错误响应
func ErrorWithData(c *gin.Context, code int, msg string, data interface{}) {
    c.JSON(http.StatusOK, Response{
        Code: code,
        Msg:  msg,
        Data: data,
    })
}

使用示例

package main

import (
    "net/http"

    "github.com/gin-gonic/gin"
)

func getUserHandler(c *gin.Context) {
    userID := c.Param("id")

    // 模拟业务逻辑
    user, err := getUserByID(userID)
    if err != nil {
        Error(c, 100101, "用户不存在")
        return
    }

    Success(c, user)
}

func createUserHandler(c *gin.Context) {
    var user User
    if err := c.ShouldBindJSON(&user); err != nil {
        Error(c, 100102, "参数错误")
        return
    }

    // 创建用户逻辑
    if err := createUser(&user); err != nil {
        Error(c, 100103, "创建用户失败")
        return
    }

    Success(c, user)
}

// 模拟数据结构和函数
type User struct {
    ID   string `json:"id"`
    Name string `json:"name"`
}

func getUserByID(id string) (*User, error) {
    // 模拟数据库查询
    if id == "1" {
        return &User{ID: "1", Name: "张三"}, nil
    }
    return nil, http.ErrMissingFile
}

func createUser(user *User) error {
    // 模拟创建用户
    return nil
}

func main() {
    r := gin.Default()

    r.GET("/user/:id", getUserHandler)
    r.POST("/user", createUserHandler)

    r.Run(":8080")
}

2. 多格式响应

现代Web应用需要支持多种响应格式以满足不同客户端需求。

完整多格式响应示例

package main

import (
    "html/template"
    "net/http"
    "os"
    "path/filepath"

    "github.com/gin-gonic/gin"
)

func main() {
    r := gin.Default()

    // 设置HTML模板
    loadTemplates(r)

    // JSON响应
    r.GET("/json", func(c *gin.Context) {
        data := gin.H{
            "name": "张三",
            "age":  30,
        }
        c.JSON(http.StatusOK, data)
    })

    // XML响应
    r.GET("/xml", func(c *gin.Context) {
        data := gin.H{
            "name": "张三",
            "age":  30,
        }
        c.XML(http.StatusOK, data)
    })

    // HTML响应
    r.GET("/html", func(c *gin.Context) {
        c.HTML(http.StatusOK, "index.tmpl", gin.H{
            "title": "欢迎页面",
            "name":  "张三",
        })
    })

    // 文件下载
    r.GET("/download", func(c *gin.Context) {
        filePath := "./downloads/sample.pdf"
        c.File(filePath)
    })

    // 文件响应(在浏览器中显示)
    r.GET("/inline", func(c *gin.Context) {
        filePath := "./files/sample.pdf"
        c.Header("Content-Disposition", "inline; filename=sample.pdf")
        c.File(filePath)
    })

    r.Run(":8080")
}

func loadTemplates(r *gin.Engine) {
    // 加载模板文件
    templatePath := "./templates/*"
    t, err := template.ParseGlob(templatePath)
    if err != nil {
        panic("加载模板失败: " + err.Error())
    }
    r.SetHTMLTemplate(t)
}

// 创建示例文件和目录
func init() {
    os.MkdirAll("./templates", os.ModePerm)
    os.MkdirAll("./downloads", os.ModePerm)
    os.MkdirAll("./files", os.ModePerm)

    // 创建示例模板文件
    templateContent := `
<!DOCTYPE html>
<html>
<head>
    <title>{{.title}}</title>
</head>
<body>
    <h1>你好, {{.name}}!</h1>
    <p>这是一个Go模板示例</p>
</body>
</html>`

    err := os.WriteFile("./templates/index.tmpl", []byte(templateContent), 0644)
    if err != nil {
        panic("创建模板文件失败: " + err.Error())
    }

    // 创建示例PDF文件
    pdfContent := []byte("%PDF-sample-content")
    os.WriteFile("./downloads/sample.pdf", pdfContent, 0644)
    os.WriteFile("./files/sample.pdf", pdfContent, 0644)
}

3. 错误码设计

良好的错误码设计能帮助开发者和用户快速定位问题。

错误码规则

我们采用6位数字错误码: - 前2位:业务模块(如10=用户模块) - 中间2位:子模块/功能(如01=登录功能) - 最后2位:具体错误类型(如01=参数错误)

错误码管理工具

package main

import "fmt"

// 错误码常量
const (
    // 用户模块错误码 (10xxxx)
    ErrUserNotFound      = 100101
    ErrUserInvalidParams = 100102
    ErrUserCreateFailed  = 100103
    ErrUserUpdateFailed  = 100104
    ErrUserDeleteFailed  = 100105

    // 订单模块错误码 (11xxxx)
    ErrOrderNotFound     = 110101
    ErrOrderInvalidState = 110102

    // 系统错误码 (99xxxx)
    ErrSystemInternal = 999001
    ErrSystemTimeout  = 999002
)

// ErrorCode 错误码管理
type ErrorCode struct {
    Code    int
    Message string
}

// ErrorCodeManager 错误码管理器
type ErrorCodeManager struct {
    errorMap map[int]string
    // 可以添加国际化支持字段
}

// NewErrorCodeManager 创建错误码管理器
func NewErrorCodeManager() *ErrorCodeManager {
    manager := &ErrorCodeManager{
        errorMap: make(map[int]string),
    }
    manager.initErrorCodes()
    return manager
}

// initErrorCodes 初始化错误码映射
func (m *ErrorCodeManager) initErrorCodes() {
    // 用户模块错误消息
    m.errorMap[ErrUserNotFound] = "用户不存在"
    m.errorMap[ErrUserInvalidParams] = "用户参数错误"
    m.errorMap[ErrUserCreateFailed] = "创建用户失败"
    m.errorMap[ErrUserUpdateFailed] = "更新用户失败"
    m.errorMap[ErrUserDeleteFailed] = "删除用户失败"

    // 订单模块错误消息
    m.errorMap[ErrOrderNotFound] = "订单不存在"
    m.errorMap[ErrOrderInvalidState] = "订单状态无效"

    // 系统错误消息
    m.errorMap[ErrSystemInternal] = "系统内部错误"
    m.errorMap[ErrSystemTimeout] = "系统请求超时"
}

// GetMessage 获取错误码对应的消息
func (m *ErrorCodeManager) GetMessage(code int) string {
    if msg, exists := m.errorMap[code]; exists {
        return msg
    }
    return "未知错误"
}

// AddErrorCode 添加自定义错误码(支持动态扩展)
func (m *ErrorCodeManager) AddErrorCode(code int, message string) {
    m.errorMap[code] = message
}

// 全局错误码管理器实例
var errorCodeManager = NewErrorCodeManager()

// GetErrorMessage 获取错误信息
func GetErrorMessage(code int) string {
    return errorCodeManager.GetMessage(code)
}

func main() {
    // 使用示例
    fmt.Printf("错误码 %d: %s\n", ErrUserNotFound, GetErrorMessage(ErrUserNotFound))
    fmt.Printf("错误码 %d: %s\n", ErrSystemInternal, GetErrorMessage(ErrSystemInternal))

    // 添加自定义错误码
    errorCodeManager.AddErrorCode(120001, "自定义业务错误")
    fmt.Printf("错误码 %d: %s\n", 120001, GetErrorMessage(120001))
}

4. 统一错误处理

通过中间件实现全局错误处理,可以捕获并统一处理所有异常。

自定义错误结构

package main

// AppError 应用错误结构
type AppError struct {
    Code    int    // 错误码
    Message string // 错误消息
    Err     error  // 原始错误(可选)
}

// Error 实现error接口
func (e *AppError) Error() string {
    if e.Err != nil {
        return fmt.Sprintf("Code: %d, Message: %s, Error: %v", e.Code, e.Message, e.Err)
    }
    return fmt.Sprintf("Code: %d, Message: %s", e.Code, e.Message)
}

// NewAppError 创建新的应用错误
func NewAppError(code int, message string, err error) *AppError {
    return &AppError{
        Code:    code,
        Message: message,
        Err:     err,
    }
}

全局错误处理中间件

package main

import (
    "fmt"
    "log"
    "net/http"
    "runtime/debug"

    "github.com/gin-gonic/gin"
)

// RecoveryMiddleware 全局恢复中间件
func RecoveryMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        defer func() {
            if err := recover(); err != nil {
                // 记录错误日志
                log.Printf("Panic recovered: %v\n%s", err, debug.Stack())

                // 转换为AppError
                var appErr *AppError
                switch e := err.(type) {
                case *AppError:
                    appErr = e
                case error:
                    appErr = NewAppError(ErrSystemInternal, "系统内部错误", e)
                default:
                    appErr = NewAppError(ErrSystemInternal, "系统内部错误", fmt.Errorf("%v", err))
                }

                // 发送错误响应
                ErrorWithData(c, appErr.Code, appErr.Message, gin.H{
                    "stack_trace": string(debug.Stack()),
                })

                c.Abort()
            }
        }()
        c.Next()
    }
}

// ErrorHandlerMiddleware 错误处理中间件
func ErrorHandlerMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        c.Next()

        // 检查是否有错误需要处理
        if len(c.Errors) > 0 {
            err := c.Errors.Last()

            var appErr *AppError
            switch e := err.Err.(type) {
            case *AppError:
                appErr = e
            default:
                appErr = NewAppError(ErrSystemInternal, "系统内部错误", e)
            }

            Error(c, appErr.Code, appErr.Message)
        }
    }
}

// 业务处理函数示例
func getUserHandler(c *gin.Context) {
    userID := c.Param("id")
    if userID == "0" {
        // 模拟业务错误
        err := NewAppError(ErrUserNotFound, "用户不存在", nil)
        c.Error(err) // 将错误添加到Gin上下文中
        return
    }

    if userID == "panic" {
        // 模拟panic
        panic("模拟panic情况")
    }

    user := &User{ID: userID, Name: "测试用户"}
    Success(c, user)
}

func main() {
    r := gin.Default()

    // 注册中间件
    r.Use(RecoveryMiddleware())
    r.Use(ErrorHandlerMiddleware())

    // 路由
    r.GET("/user/:id", getUserHandler)

    // 启动服务器
    r.Run(":8080")
}

完整示例

下面是一个完整的示例,整合了所有概念:

package main

import (
    "fmt"
    "log"
    "net/http"
    "runtime/debug"

    "github.com/gin-gonic/gin"
)

// 错误码常量
const (
    ErrUserNotFound      = 100101
    ErrUserInvalidParams = 100102
    ErrSystemInternal    = 999001
)

// Response 通用响应结构
type Response struct {
    Code int         `json:"code"`
    Msg  string      `json:"msg"`
    Data interface{} `json:"data,omitempty"`
}

// AppError 应用错误结构
type AppError struct {
    Code    int
    Message string
    Err     error
}

func (e *AppError) Error() string {
    if e.Err != nil {
        return fmt.Sprintf("Code: %d, Message: %s, Error: %v", e.Code, e.Message, e.Err)
    }
    return fmt.Sprintf("Code: %d, Message: %s", e.Code, e.Message)
}

func NewAppError(code int, message string, err error) *AppError {
    return &AppError{
        Code:    code,
        Message: message,
        Err:     err,
    }
}

// 成功响应
func Success(c *gin.Context, data interface{}) {
    c.JSON(http.StatusOK, Response{
        Code: 0,
        Msg:  "success",
        Data: data,
    })
}

// 错误响应
func Error(c *gin.Context, code int, msg string) {
    c.JSON(http.StatusOK, Response{
        Code: code,
        Msg:  msg,
        Data: nil,
    })
}

// 恢复中间件
func RecoveryMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        defer func() {
            if err := recover(); err != nil {
                log.Printf("Panic recovered: %v\n%s", err, debug.Stack())

                var appErr *AppError
                switch e := err.(type) {
                case *AppError:
                    appErr = e
                case error:
                    appErr = NewAppError(ErrSystemInternal, "系统内部错误", e)
                default:
                    appErr = NewAppError(ErrSystemInternal, "系统内部错误", fmt.Errorf("%v", err))
                }

                Error(c, appErr.Code, appErr.Message)
                c.Abort()
            }
        }()
        c.Next()
    }
}

// 错误处理中间件
func ErrorHandlerMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        c.Next()

        if len(c.Errors) > 0 {
            err := c.Errors.Last()

            var appErr *AppError
            switch e := err.Err.(type) {
            case *AppError:
                appErr = e
            default:
                appErr = NewAppError(ErrSystemInternal, "系统内部错误", e)
            }

            Error(c, appErr.Code, appErr.Message)
        }
    }
}

// 用户结构
type User struct {
    ID   string `json:"id"`
    Name string `json:"name"`
}

// 获取用户处理函数
func getUserHandler(c *gin.Context) {
    userID := c.Param("id")

    if userID == "0" {
        err := NewAppError(ErrUserNotFound, "用户不存在", nil)
        c.Error(err)
        return
    }

    if userID == "panic" {
        panic("模拟panic情况")
    }

    user := &User{ID: userID, Name: "测试用户"}
    Success(c, user)
}

func main() {
    r := gin.Default()

    // 注册中间件
    r.Use(RecoveryMiddleware())
    r.Use(ErrorHandlerMiddleware())

    // 路由
    r.GET("/user/:id", getUserHandler)

    // 启动服务器
    fmt.Println("服务器启动在 :8080")
    r.Run(":8080")
}

总结

通过本教程,我们学习了:

  1. 响应格式标准化:创建统一的响应结构,提高API一致性
  2. 多格式响应:支持JSON、XML、HTML和文件响应
  3. 错误码设计:实现可维护的错误码管理系统
  4. 统一错误处理:通过中间件实现全局错误捕获和处理

这些技术可以帮助你构建更加健壮、可维护的Go Web应用程序。记住,良好的错误处理和响应机制是高质量API的标志。