Gin:自定义日志、验证器与中间件全指南
前言
在使用 Gin 开发 Web 服务时,默认的功能已经能覆盖大部分场景,但在生产环境中我们往往需要更精细的控制——比如定制日志格式以便于 ELK 采集、增加业务专属的参数校验规则、或者编写通用的请求拦截中间件。Gin 本身提供了非常优雅的扩展机制,本文将结合四个实际示例,带你彻底掌握 Gin 中四个常用自定义能力的实现方法。
自定义HTTP访问日志格式
Gin默认的访问日志长这样:
[GIN] 2023/04/19 - 10:41:52 | 200 | 122.767µs | ::1 | GET "/ping"虽然信息完整,但格式较为固定。如果我们需要与现有日志平台对接,或希望记录更多字段(例如 User-Agent、错误信息等),可以使用gin.LoggerWithFormatter自定义日志输出格式。
func main() { router := gin.New() // 使用自定义格式的日志中间件 router.Use(gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string { // 返回你想要的日志行格式 return fmt.Sprintf("%s - [%s] \"%s %s %s %d %s \"%s\" %s\"\n", param.ClientIP, //用户ipv6地址 param.TimeStamp.Format(time.RFC1123), //访问时间 param.Method, //访问方法 param.Path, //访问路径 param.Request.Proto, //http版本 param.StatusCode, //状态码 param.Latency, //请求处理耗时 param.Request.UserAgent(), //客户端User-Agent头 param.ErrorMessage, //错误信息 ) })) router.Use(gin.Recovery()) router.GET("/ping", func(c *gin.Context) { c.String(200, "pong") }) router.Run(":8080") }如果想输出到文件中,可以这样做:
f, _ := os.Create("gin.log") gin.DefaultWriter = f自定义路由日志
在启动 Gin 服务时,控制台会打印所有注册的路由信息:
[GIN-debug] POST /foo --> main.main.func1 (3 handlers) [GIN-debug] GET /bar --> main.main.func2 (3 handlers) [GIN-debug] GET /status --> main.main.func3 (3 handlers)这些信息对调试很有帮助,但格式无法变更。Gin 提供了gin.DebugPrintRouteFunc变量,允许我们接管路由日志的输出。
func main() { r := gin.Default() gin.DebugPrintRouteFunc = func(httpMethod, absolutePath, handlerName string, nuHandlers int) { // 方法,资源路径,函数名,函数个数 log.Printf("endpoint %v %v %v %v\n", httpMethod, absolutePath, handlerName, nuHandlers) } r.POST("/foo", func(c *gin.Context) { c.JSON(http.StatusOK, "foo") }) r.GET("/bar", func(c *gin.Context) { c.JSON(http.StatusOK, "bar") }) r.GET("/status", func(c *gin.Context) { c.JSON(http.StatusOK, "status") }) r.Run() }示例输出:
[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached. [GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production. - using env: export GIN_MODE=release - using code: gin.SetMode(gin.ReleaseMode) [GIN-debug] [WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value. Please check https://github.com/gin-gonic/gin/blob/master/docs/doc.md#dont-trust-all-proxies for details. [GIN-debug] Environment variable PORT is undefined. Using port :8080 by default [GIN-debug] Listening and serving HTTP on :8080 2026/04/19 10:41:52 endpoint POST /foo main.main.func2 3 2026/04/19 10:41:52 endpoint GET /bar main.main.func3 3 2026/04/19 10:41:52 endpoint GET /status main.main.func4 3可以看到,最后三行就是我们自己设置的路由日志了
自定义参数验证器
Gin 内置的binding标签提供了常用验证规则(如required、email、gt等),但在实际业务中我们常常需要定制校验逻辑,比如判断日期是否晚于今天、字段值是否在指定范围内等。
下面我们为Booking结构体的日期字段实现一个bookabledate验证规则,要求日期不能早于今天。
func main() { r := gin.Default() if v, ok := binding.Validator.Engine().(*validator.Validate); ok { v.RegisterValidation("bookabledate", bookleDate) } r.GET("/bookdate", getBookable) r.Run(":8080") } // 验证输入输出, // required:字段不能缺失;bookabledate:自定义规则,由后续函数实现; // gtfield=CheckIn:CheckOut必须大于CheckIn,time_format:指定日期格式2006-01-02 type booking struct { CheckIn time.Time `form:"check_in" binding:"required,bookabledate" time_format:"2006-01-02"` CheckOut time.Time `form:"check_out" binding:"required,gtfield=CheckIn,bookabledate" time_format:"2006-01-02"` } // 验证实现函数,日期要晚于今日 var bookleDate validator.Func = func(f validator.FieldLevel) bool { date, ok := f.Field().Interface().(time.Time) if ok { today := time.Now() // 验证日期是否在date之后 if today.After(date) { return false } } return true } func getBookable(c *gin.Context) { var b booking if err := c.ShouldBindWith(&b, binding.Query); err == nil { c.JSON(http.StatusOK, gin.H{"message": "时间验证通过"}) } else { c.JSON(http.StatusBadRequest, gin.H{"error": "时间要在今天之后"}) } }自定义中间件
中间件是 Gin 最强大的功能之一。通过编写自定义中间件,我们可以实现请求计时、鉴权、日志记录、请求体修改等通用逻辑。
中间件执行流程:
当一个请求到达 Gin 时,它会按顺序执行所有已注册的中间件。如果在中间件中调用了c.Next(),则会先执行后续的中间件和最终的路由处理函数,待它们全部返回后再继续执行c.Next()后面的代码。
func main() { // 创建一个没有中间件的gin引擎 r := gin.New() // 添加自定义的中间件 r.Use(Logger()) r.Use(SetDate()) // 设置响应路由 r.GET("/:name", func(c *gin.Context) { // 从上下文获取值 say := c.MustGet("say").(string) // 解析uri中的参数 name := c.Param("name") // 由于处理太快,输出日志总是0s,所以sleep 1纳秒 time.Sleep(1) c.String(http.StatusOK, "Hello %s,%s", name, say) }) r.Run(":8080") } // 自定义中间件:自定义一个计算其他所有中间件总耗时的中间件 func Logger() gin.HandlerFunc { return func(c *gin.Context) { // 记录请求的时间 t := time.Now() // 暂停当前函数,调用后续所有中间件和路由处理函数 c.Next() // 计算总耗时 latency := time.Since(t) // 输出所用时间 log.Print(latency) } } // 自定义中间件,用于在上下文写入数据供后续使用 func SetDate() gin.HandlerFunc { return func(c *gin.Context) { c.Set("say", "欢迎来到本站!") } }输出结果:
2026/04/19 11:26:45 508.6µs可以看到,通过自定义各种中间件,可以得到自己想要的数据或完成想要的功能,比如看请求到响应完成的总耗时。
总结
Gin 的扩展性设计非常优秀,通过上述四个方向的自定义能力,开发者可以轻松实现:
| 自定义方向 | 核心 API / 方法 | 主要用途 |
|---|---|---|
| 访问日志格式 | gin.LoggerWithFormatter | 定制 HTTP 请求日志样式和输出目标 |
| 路由注册日志 | gin.DebugPrintRouteFunc | 修改启动时路由信息的打印方式 |
| 参数验证规则 | binding.Validator.Engine().RegisterValidation | 添加业务专属的参数校验逻辑 |
| 中间件 | gin.HandlerFunc+c.Next() | 实现请求拦截、预处理、后处理 |
掌握这些自定义技巧,能够让你的 Gin 应用更加健壮、可观测、易维护,更好地适应生产环境的复杂需求。
