golang 写一个类似于springboot 的异常处理

作者: adm 分类: go 发布时间: 2025-04-09

在 Go 中实现类似 Spring Boot 的全局异常处理机制。Go 没有内置的异常处理(try-catch),但我们可以通过中间件模式实现统一的错误处理。

1. 定义统一的响应结构

go

package response

import "time"

// ApiResult 统一返回结果
type ApiResult struct {
    Code      int         `json:"code"`
    Message   string      `json:"message"`
    Data      interface{} `json:"data"`
    Timestamp int64       `json:"timestamp"`
}

// NewApiResult 创建新的响应
func NewApiResult(code int, message string, data interface{}) *ApiResult {
    return &ApiResult{
        Code:      code,
        Message:   message,
        Data:      data,
        Timestamp: time.Now().UnixMilli(),
    }
}

// Success 成功响应
func Success(data interface{}) *ApiResult {
    return NewApiResult(200, "success", data)
}

// Error 错误响应
func Error(code int, message string) *ApiResult {
    return NewApiResult(code, message, nil)
}

// Errorf 格式化错误响应
func Errorf(code int, format string, args ...interface{}) *ApiResult {
    return NewApiResult(code, fmt.Sprintf(format, args...), nil)
}

2. 自定义错误类型

go

package exception

// BusinessError 业务错误接口
type BusinessError interface {
    error
    Code() int
}

// businessError 业务错误实现
type businessError struct {
    code    int
    message string
}

func (e *businessError) Error() string {
    return e.message
}

func (e *businessError) Code() int {
    return e.code
}

// NewBusinessError 创建业务错误
func NewBusinessError(code int, message string) error {
    return &businessError{
        code:    code,
        message: message,
    }
}

// NewBadRequestError 创建400错误
func NewBadRequestError(message string) error {
    return NewBusinessError(400, message)
}

// NewNotFoundError 创建404错误
func NewNotFoundError(message string) error {
    return NewBusinessError(404, message)
}

// NewInternalServerError 创建500错误
func NewInternalServerError(message string) error {
    return NewBusinessError(500, message)
}

// IsBusinessError 判断是否为业务错误
func IsBusinessError(err error) bool {
    _, ok := err.(BusinessError)
    return ok
}

// GetErrorCode 获取错误码
func GetErrorCode(err error) int {
    if be, ok := err.(BusinessError); ok {
        return be.Code()
    }
    return 500 // 默认服务器错误
}

3. 错误处理中间件

go

package middleware

import (
    "log"
    "net/http"
    "runtime/debug"
    
    "your-project/response"
    "your-project/exception"
)

// ErrorHandler 错误处理中间件
func ErrorHandler(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        defer func() {
            if err := recover(); err != nil {
                // 处理 panic
                log.Printf("panic recovered: %v\n%s", err, debug.Stack())
                
                w.Header().Set("Content-Type", "application/json")
                w.WriteHeader(http.StatusInternalServerError)
                
                result := response.Error(500, "服务器内部错误")
                json.NewEncoder(w).Encode(result)
            }
        }()
        
        // 创建自定义 ResponseWriter 来捕获错误
        crw := &CustomResponseWriter{
            ResponseWriter: w,
            statusCode:     http.StatusOK,
        }
        
        next.ServeHTTP(crw, r)
        
        // 这里可以处理未捕获的错误
        if crw.statusCode >= 400 {
            // 已经有错误响应了,不需要额外处理
        }
    })
}

// CustomResponseWriter 自定义响应写入器
type CustomResponseWriter struct {
    http.ResponseWriter
    statusCode int
}

func (w *CustomResponseWriter) WriteHeader(code int) {
    w.statusCode = code
    w.ResponseWriter.WriteHeader(code)
}

4. 断言工具

go

package util

import "your-project/exception"

// Assert 断言工具
type Assert struct{}

// NotNull 断言对象不为空
func (Assert) NotNull(obj interface{}, message string) {
    if obj == nil {
        panic(exception.NewBadRequestError(message))
    }
}

// NotEmpty 断言字符串不为空
func (Assert) NotEmpty(str string, message string) {
    if str == "" {
        panic(exception.NewBadRequestError(message))
    }
}

// IsTrue 断言条件为真
func (Assert) IsTrue(condition bool, message string) {
    if !condition {
        panic(exception.NewBadRequestError(message))
    }
}

// NoError 断言没有错误
func (Assert) NoError(err error, message string) {
    if err != nil {
        panic(exception.NewBadRequestError(message + ": " + err.Error()))
    }
}

5. 控制器示例

go

package controller

import (
    "encoding/json"
    "net/http"
    "strconv"
    
    "your-project/response"
    "your-project/util"
    "your-project/service"
)

type UserController struct {
    userService *service.UserService
    assert      util.Assert
}

func NewUserController() *UserController {
    return &UserController{
        userService: service.NewUserService(),
    }
}

// GetUser 获取用户
func (c *UserController) GetUser(w http.ResponseWriter, r *http.Request) {
    // 从路径参数获取ID
    vars := mux.Vars(r)
    idStr := vars["id"]
    
    // 转换ID
    id, err := strconv.ParseInt(idStr, 10, 64)
    
    // 使用断言
    c.assert.NoError(err, "无效的用户ID")
    c.assert.NotNull(id, "用户ID不能为空")
    
    // 调用服务
    user, err := c.userService.GetUser(id)
    if err != nil {
        // 错误会被中间件捕获
        panic(err)
    }
    
    // 返回成功响应
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(response.Success(user))
}

// CreateUser 创建用户
func (c *UserController) CreateUser(w http.ResponseWriter, r *http.Request) {
    var req struct {
        Name  string `json:"name"`
        Email string `json:"email"`
        Age   int    `json:"age"`
    }
    
    // 解析请求体
    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
        panic(exception.NewBadRequestError("无效的请求参数"))
    }
    
    // 参数校验
    c.assert.NotEmpty(req.Name, "用户名不能为空")
    c.assert.NotEmpty(req.Email, "邮箱不能为空")
    c.assert.IsTrue(req.Age > 0, "年龄必须大于0")
    
    // 调用服务
    user, err := c.userService.CreateUser(req.Name, req.Email, req.Age)
    if err != nil {
        panic(err)
    }
    
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(http.StatusCreated)
    json.NewEncoder(w).Encode(response.Success(user))
}

6. 服务层示例

go

package service

import (
    "fmt"
    
    "your-project/exception"
    "your-project/model"
    "your-project/repository"
)

type UserService struct {
    userRepo *repository.UserRepository
}

func NewUserService() *UserService {
    return &UserService{
        userRepo: repository.NewUserRepository(),
    }
}

// GetUser 获取用户
func (s *UserService) GetUser(id int64) (*model.User, error) {
    // 业务逻辑
    user, err := s.userRepo.FindById(id)
    if err != nil {
        return nil, exception.NewNotFoundError(fmt.Sprintf("用户不存在: %d", id))
    }
    
    return user, nil
}

// CreateUser 创建用户
func (s *UserService) CreateUser(name, email string, age int) (*model.User, error) {
    // 业务校验
    if age < 0 || age > 150 {
        return nil, exception.NewBadRequestError("年龄必须在0-150之间")
    }
    
    // 检查邮箱是否已存在
    exists, err := s.userRepo.ExistsByEmail(email)
    if err != nil {
        return nil, exception.NewInternalServerError("数据库查询失败")
    }
    if exists {
        return nil, exception.NewBadRequestError("邮箱已存在")
    }
    
    // 创建用户
    user := &model.User{
        Name:  name,
        Email: email,
        Age:   age,
    }
    
    if err := s.userRepo.Save(user); err != nil {
        return nil, exception.NewInternalServerError("创建用户失败")
    }
    
    return user, nil
}

7. 路由设置和中间件应用

go

package main

import (
    "log"
    "net/http"
    
    "github.com/gorilla/mux"
    
    "your-project/controller"
    "your-project/middleware"
)

func main() {
    r := mux.NewRouter()
    
    // 应用错误处理中间件(全局)
    r.Use(middleware.ErrorHandler)
    
    // 初始化控制器
    userController := controller.NewUserController()
    
    // 注册路由
    r.HandleFunc("/users/{id}", userController.GetUser).Methods("GET")
    r.HandleFunc("/users", userController.CreateUser).Methods("POST")
    
    // 启动服务器
    log.Println("Server starting on :8080")
    log.Fatal(http.ListenAndServe(":8080", r))
}

8. 完整示例:main.go

go

package main

import (
    "encoding/json"
    "log"
    "net/http"
    
    "github.com/gorilla/mux"
    
    "your-project/middleware"
    "your-project/response"
)

// 简单的健康检查接口
func healthCheck(w http.ResponseWriter, r *http.Request) {
    json.NewEncoder(w).Encode(response.Success("service is running"))
}

// 测试断言
func testAssert(w http.ResponseWriter, r *http.Request) {
    // 模拟参数为空的情况
    var name string
    
    // 使用断言
    util.Assert{}.NotEmpty(name, "name不能为空")
    
    json.NewEncoder(w).Encode(response.Success("ok"))
}

func main() {
    r := mux.NewRouter()
    
    // 全局错误处理中间件
    r.Use(middleware.ErrorHandler)
    
    // 路由
    r.HandleFunc("/health", healthCheck).Methods("GET")
    r.HandleFunc("/test/assert", testAssert).Methods("GET")
    
    log.Println("Server starting on :8080")
    log.Fatal(http.ListenAndServe(":8080", r))
}

9. 测试请求和响应

正常请求

curl http://localhost:8080/health

响应:

{
    "code": 200,
    "message": "success",
    "data": "service is running",
    "timestamp": 1645861234567
}

触发断言的请求

curl http://localhost:8080/test/assert

响应:

{
    "code": 400,
    "message": "name不能为空",
    "data": null,
    "timestamp": 1645861234567
}

404 请求

curl http://localhost:8080/not-found

响应(会被错误中间件捕获):

{
    "code": 404,
    "message": "404 page not found",
    "data": null,
    "timestamp": 1645861234567
}

总结

这种实现方式与 Spring Boot 的对比:

Spring Boot	Go 实现
@RestControllerAdvice	中间件 (middleware)
@ExceptionHandler	panic + recover
Assert.notNull	自定义 Assert 工具
ResponseEntity	ApiResult 结构体
BusinessException	BusinessError 接口

关键点:

中间件模式:通过中间件统一捕获 panic 和错误
自定义错误类型:实现错误码和错误信息的封装
断言工具:提供便捷的参数校验
统一响应格式:确保所有响应格式一致

如果觉得我的文章对您有用,请随意赞赏。您的支持将鼓励我继续创作!