Gin框架基础篇008_错误处理机制详解

Gin框架提供了强大而灵活的错误处理机制,包括内置的错误管理、panic恢复、中间件支持等功能。通过合理使用这些机制,我们可以构建出健壮、可维护的Web应用程序。在实际开发中,应根据项目需求设计合适的错误处理策略,确保错误能够被正确捕获、处理和记录。

本文将全面详细地介绍Gin框架中的错误处理机制以及相关的方法。

1. 错误处理基础

1.1. gin.Error

1.1.1. gin.Error 类型

Gin框架提供了一个内置的gin.Error结构体来处理错误。这个结构体不仅包含错误信息本身,还包含错误类型和元数据,使开发者能够更好地管理错误。

类型源码:

// gin.Error 定义了一种错误的格式
type Error struct {
	Err  error // error
	Type ErrorType // 错误类型
	Meta any // 元数据
}

// errorMsgs 定义了gin.Error数组类型
type errorMsgs []*Error

1.1.2. 错误类型

gin.Error中的ErrorType属性用于标识错误的类型。

// ErrorType is an unsigned 64-bit error code as defined in the gin spec.
type ErrorType uint64

const (
	// ErrorTypeBind is used when Context.Bind() fails.
	ErrorTypeBind ErrorType = 1 << 63
	// ErrorTypeRender is used when Context.Render() fails.
	ErrorTypeRender ErrorType = 1 << 62
	// ErrorTypePrivate indicates a private error.
	ErrorTypePrivate ErrorType = 1 << 0
	// ErrorTypePublic indicates a public error.
	ErrorTypePublic ErrorType = 1 << 1
	// ErrorTypeAny indicates any other error.
	ErrorTypeAny ErrorType = 1<<64 - 1
	// ErrorTypeNu indicates any other error.
	ErrorTypeNu = 2
)

gin使用位标志来区分不同类型的错误,通过位运算实现错误类型的组合和判断!

内置的错误类型:

  • ErrorTypeBind:绑定数据时发生的错误
  • ErrorTypeRender:渲染响应时发生的错误
  • ErrorTypePrivate:私有错误类型
  • ErrorTypePublic:公共错误类型
  • ErrorTypeAny:任意错误类型
  • ErrorTypeNu: 备用的错误类型

1.1.3. 类型方法

gin.Error类型的方法如下:

  • (msg *Error) SetType(flags ErrorType) *Error:设置错误类型。
  • (msg *Error) SetMeta(data any) *Error:设置错误的元数据。
  • (msg *Error) JSON() any:创建一个适合格式化的JSON对象。
  • (msg *Error) MarshalJSON() ([]byte, error):序列化成JSON字符串。
  • (msg Error) Error() string:获取错误字符串。
  • (msg *Error) IsType(flags ErrorType) bool:判断错误是否属于指定的错误类型。
  • (msg Error) Unwrap() error:获取原始错误。

errorMsgs类型的方法如下:

  • (a errorMsgs) ByType(typ ErrorType) errorMsgs :筛选指定错误类型的错误。
  • (a errorMsgs) Last() *Error:获取最后一个错误。
  • (a errorMsgs) Errors() []string:获取错误字符串数组。
  • (a errorMsgs) JSON() any:创建一个适合格式化的JSON对象。
  • (a errorMsgs) MarshalJSON() ([]byte, error):序列化成JSON字符串。
  • (a errorMsgs) String() string:获取错误字符串。

1.2. 上下文中的错误列表

gin.Context结构体中,包含一个Errors errorMsgs属性,用于保存所有处理器/中间件附加到上下文的错误列表。

type Context struct {
	...

	// Errors is a list of errors attached to all the handlers/middlewares who used this context.
	Errors errorMsgs

	...
}

1.3. 向上下文中添加错误

在处理请求时,可以使用Context.Error()方法向错误列表中添加错误:

方法源码

// Error 用于往上下文错误列表中添加错误
func (c *Context) Error(err error) *Error {
	if err == nil {
		panic("err is nil")
	}

  // 判断error是否为gin.Error类型
	var parsedError *Error
	ok := errors.As(err, &parsedError)
	if !ok {
    // 如果不是就包装成一个gin.Error类型
		parsedError = &Error{
			Err:  err,
			Type: ErrorTypePrivate,
		}
	}

  // 添加到错误列表
	c.Errors = append(c.Errors, parsedError)
	return parsedError
}

示例:

func handler(c *gin.Context) {
    if err := someOperation(); err != nil {
        c.Error(err)  // 添加错误到错误列表
        c.JSON(500, gin.H{"error": err.Error()})
        return
    }
    c.JSON(200, gin.H{"status": "success"})
}

1.4. 获取错误列表

通过Context.Errors可以获取当前请求的所有错误:

func handler(c *gin.Context) {
    // ... 一些操作 ...
    
    if len(c.Errors) > 0 {
        // 处理错误
        for _, e := range c.Errors {
            log.Printf("Error: %v, Type: %v, Meta: %v", e.Err, e.Type, e.Meta)
        }
    }
}

2. 错误处理实践

2.1. 错误处理中间件

为了避免重复编写错误处理代码,我们可以将通用的错误处理逻辑写到中间件中。

错误处理中间件示例:


// ErrorsMiddleware 错误处理中间件
func ErrorsMiddleware() gin.HandlerFunc {
	return func(c *gin.Context) {
		// 先执行后续处理函数
		c.Next()

		// 错误检查
		if len(c.Errors) == 0 {
			return
		}

		// 只处理最后一个错误
		lastError := c.Errors.Last()
		switch lastError.Type {
		case gin.ErrorTypeBind:
			c.JSON(http.StatusBadRequest, gin.H{
				"error":  "Binding Error",
				"detail": lastError.Error(),
			})
		case gin.ErrorTypeRender:
			c.JSON(http.StatusInternalServerError, gin.H{
				"error":  "Render Error",
				"detail": lastError.Error(),
			})
		default:
			c.JSON(http.StatusInternalServerError, gin.H{
				"error":  "Internal Server Error",
				"detail": lastError.Error(),
			})
		}
	}
}

业务逻辑示例:

func TestError(c *gin.Context) {
	param := new(Param)
	if err := c.ShouldBind(param); err != nil {
		_ = c.Error(&gin.Error{
			Err:  err,
			Type: gin.ErrorTypeBind,
		})
		return
	}

	c.JSON(200, gin.H{
		"name": param.Name,
		"age":  param.Age,
	})
}

2.2. 自定义错误类型

gin框架提供的错误类型并不总能满足我们的业务需求,我们可以定义自己的错误类型和处理逻辑。

比如我们可以定义如下的一个错误类型:

// AppError 错误结构体
type AppError struct {
	status int    // 状态码
	Code   int    `json:"code"` // 错误码
	Msg    string `json:"msg"`  // 错误信息
}

func (err *AppError) Error() string {
	return fmt.Sprintf("status: %d, code: %d, msg: %s", err.status, err.Code, err.Msg)
}

错误中间件示例:


// ErrorsMiddleware 错误处理中间件
func ErrorsMiddleware() gin.HandlerFunc {
	return func(c *gin.Context) {
		// 先执行后续处理函数
		c.Next()

		// 错误检查
		if len(c.Errors) == 0 {
			return
		}

		// 只处理最后一个错误
		lastError := c.Errors.Last()
		var appError *model.AppError
		if errors.As(lastError.Err, &appError) {
			c.JSON(appError.Status, gin.H{
				"code":    appError.Code,
				"message": appError.Msg,
			})
		} else {
			c.JSON(500, gin.H{
				"code":    "E0000",
				"message": "Internal System Error",
			})
		}
	}
}

业务逻辑示例:

func TestError(c *gin.Context) {
	param := new(Param)
  // 绑定参数
	if err := c.ShouldBind(param); err != nil {
		var validatorErrs validator.ValidationErrors
		if errors.As(err, &validatorErrs) {
      // 如果是校验出错,返回V0001
			_ = c.Error(&model.AppError{
				Status: 400,
				Code:   "V0001",
				Msg:    validatorErrs.Error(),
			})
		} else {
      // 其他错误返回V0000
			_ = c.Error(&model.AppError{
				Status: 400,
				Code:   "V0000",
				Msg:    err.Error(),
			})
		}
		return
	}

	c.JSON(200, gin.H{
		"name": param.Name,
		"age":  param.Age,
	})
}

2.3. 自定义错误页面

在Web应用中,我们可能需要自定义错误页面。这通常与HTML模板一起使用。

示例:

func main() {
    r := gin.Default()
    r.LoadHTMLGlob("templates/*")
    
    r.Use(func(c *gin.Context) {
        c.Next()
        
        if len(c.Errors) > 0 {
            err := c.Errors.Last()
            c.HTML(500, "error.tmpl", gin.H{
                "error": err.Error(),
                "status": 500,
            })
        }
    })
    
    r.GET("/error", func(c *gin.Context) {
        c.Error(errors.New("something went wrong"))
    })
    
    r.Run()
}

对应的模板文件 error.tmpl

<!DOCTYPE html>
<html>
<head>
    <title>Error</title>
</head>
<body>
    <h1>Error: {{ .status }}</h1>
    <p>{{ .error }}</p>
    <a href="/">Go Home</a>
</body>
</html>

2.4. 日志记录错误

错误处理通常与日志记录相结合,以便于调试和监控:

示例:

import (
    "log"
    "time"
)

func LoggingMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        start := time.Now()
        
        c.Next()
        
        duration := time.Since(start)
        
        // 记录错误
        if len(c.Errors) > 0 {
            for _, e := range c.Errors {
                log.Printf("Error: %s | Status: %d | Method: %s | Path: %s | Duration: %v",
                    e.Error(),
                    c.Writer.Status(),
                    c.Request.Method,
                    c.Request.URL.Path,
                    duration,
                )
            }
        }
    }
}

func main() {
    r := gin.New()
    r.Use(LoggingMiddleware())
    
    r.GET("/test", func(c *gin.Context) {
        c.Error(errors.New("test error"))
        c.JSON(200, gin.H{"message": "Hello"})
    })
    
    r.Run()
}

3. panic与恢复

3.1. 默认 Recovery 中间件

Gin框架内置了Recovery()中间件,用于从panic中恢复,防止程序崩溃。

源码:

func Recovery() HandlerFunc {
	return RecoveryWithWriter(DefaultErrorWriter)
}

Recovery方法返回一个中间件,它会捕获任意panic。如果有panic,还会应答HTTP Code 500。

示例:

func main() {
    r := gin.Default()  // 默认包含 Logger 和 Recovery 中间件
    r.GET("/panic", func(c *gin.Context) {
        panic("something went wrong!")
    })
    r.Run()
}

3.2. 自定义 Recovery 处理

如果需要自定义panic处理逻辑,可以使用如下的几个方法:

  • gin.CustomRecovery:返回Recovery处理中间件,支持自定义处理方法。
  • gin.RecoveryWithWriter:返回Recovery处理中间件,支持自定义输出writer和处理方法,处理方法为可变参数。
  • gin.CustomRecoveryWithWriter:返回Recovery处理中间件,支持自定义输出writer和处理方法。。

源码:

// 支持自定义处理方法
func CustomRecovery(handle RecoveryFunc) HandlerFunc {
	return RecoveryWithWriter(DefaultErrorWriter, handle)
}

// 支持自定义输出writer和处理方法,处理方法为可变参数。
func RecoveryWithWriter(out io.Writer, recovery ...RecoveryFunc) HandlerFunc {
	if len(recovery) > 0 {
		return CustomRecoveryWithWriter(out, recovery[0])
	}
	return CustomRecoveryWithWriter(out, defaultHandleRecovery)
}

// 支持自定义输出writer和处理方法
func CustomRecoveryWithWriter(out io.Writer, handle RecoveryFunc) HandlerFunc {
	var logger *log.Logger
	if out != nil {
		logger = log.New(out, "\n\n\x1b[31m", log.LstdFlags)
	}
	return func(c *Context) {
		defer func() {
			if err := recover(); err != nil {
				// Check for a broken connection, as it is not really a
				// condition that warrants a panic stack trace.
				var brokenPipe bool
				if ne, ok := err.(*net.OpError); ok {
					var se *os.SyscallError
					if errors.As(ne, &se) {
						seStr := strings.ToLower(se.Error())
						if strings.Contains(seStr, "broken pipe") ||
							strings.Contains(seStr, "connection reset by peer") {
							brokenPipe = true
						}
					}
				}
				if logger != nil {
					const stackSkip = 3
					if brokenPipe {
						logger.Printf("%s\n%s%s", err, secureRequestDump(c.Request), reset)
					} else if IsDebugging() {
						logger.Printf("[Recovery] %s panic recovered:\n%s\n%s\n%s%s",
							timeFormat(time.Now()), secureRequestDump(c.Request), err, stack(stackSkip), reset)
					} else {
						logger.Printf("[Recovery] %s panic recovered:\n%s\n%s%s",
							timeFormat(time.Now()), err, stack(stackSkip), reset)
					}
				}
				if brokenPipe {
					// If the connection is dead, we can't write a status to it.
					c.Error(err.(error)) //nolint: errcheck
					c.Abort()
				} else {
					handle(c, err)
				}
			}
		}()
		c.Next()
	}
}

示例:

func main() {
    r := gin.New()
    
    // 自定义 Recovery 处理函数
    r.Use(gin.RecoveryWithWriter(gin.DefaultErrorWriter, func(c *gin.Context, err interface{}) {
        // 自定义错误处理逻辑
        c.JSON(500, gin.H{
            "error": "Internal Server Error",
            "message": "Something went wrong!",
        })
    }))
    
    r.GET("/panic", func(c *gin.Context) {
        panic("something went wrong!")
    })
    r.Run()
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值