【Go】基于 Gin 从0到1搭建 Web 管理后台系统后端服务(三)路由、自定义校验器和 Redis

本文涉及的产品
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
云数据库 Tair(兼容Redis),内存型 2GB
简介: 【Go】基于 Gin 从0到1搭建 Web 管理后台系统后端服务(三)路由、自定义校验器和 Redis

image.png本文正在参加「金石计划」


flag:每月至少产出三篇高质量文章~


在之前已经基于 React18+TS4.x+Webpack5 从0到1搭建了一个 React 基本项目架子,并在 npm 上发布了我们的脚手架,具体的步骤见下面四篇:



【脚手架】从0到1搭建React18+TS4.x+Webpack5项目(一)项目初始化

【脚手架】从0到1搭建React18+TS4.x+Webpack5项目(二)基础功能配置

【脚手架】从0到1搭建React18+TS4.x+Webpack5项目(三)代码质量和git提交规范

【脚手架】从0到1搭建React18+TS4.x+Webpack5项目(四)发布脚手架



接下来,我将用几篇文章介绍如何基于 Go 语言搭建一个后端的基础架子。然后前后端同步进行开发,后端服务基于 Gin + Gorm + Casbin,前端则是基于 React + antd,开发一套常见的基于 RBAC 权限控制的前后端分离的全栈管理后台项目,手把手带你入门前后端开发。第一篇:


【Go】基于 Gin 从0到1搭建 Web 管理后台系统后端服务(一)项目初始化、配置和日志

【Go】基于 Gin 从0到1搭建 Web 管理后台系统后端服务(二)连接数据库


已经完成,接下来进入第三篇,将路由、自定义校验器和 Redis的引入。


源码:gin_common_web_server - branch:cha- 02


1、路由

将我们 core/server.go 中启动服务的方法改造一下,在这里面做路由的初始化:

func RunServer() {
   // 初始化路由
   Router := initialize.Routers()
   address := fmt.Sprintf(":%d", global.EWA_CONFIG.App.Port)
   s := initServer(address, Router)
   global.EWA_LOG.Info("server run success on ", zap.String("address", address))
   // 保证文本顺序输出
   time.Sleep(10 * time.Microsecond)
   global.EWA_LOG.Error(s.ListenAndServe().Error())
}

1.1 路由初始化

initialize 下新建 router.go 文件,实现路由初始化方法:

package initialize
import (
   "ewa_admin_server/router"
   "net/http"
   "github.com/gin-gonic/gin"
)
func Routers() *gin.Engine {
   Router := gin.Default()
   systemRouter := router.RouterGroupApp.System
   PublicGroup := Router.Group("")
   {
      // 健康监测
      PublicGroup.GET("/health", func(c *gin.Context) {
         c.JSON(http.StatusOK, "ok")
      })
   }
   {
      systemRouter.InitBaseRouter(PublicGroup) // 注册基础功能路由 不做鉴权
   }
   return Router
}

在根目录下新建一个 router 文件来管理所有的路由,并且每一级分组路由都统一用一个 enter.go 来管理:image.png
Go 语言中,文件属于一个 package(包), 包是组织相关代码的一种方式。为了让外部调用者能够使用包中的代码,需要将其暴露出去。假设有一个文件(例如 enter.go),其中定义了若干个函数和变量,但是这些函数和变量的命名规则可能不同,也可能会有命名冲突。为了更好地组织代码、减少冲突并提高代码可读性和可维护性,可以将这些函数和变量统一在该文件中使用一个模块(module)来向外部进行暴露。

这样做的好处是可以通过该模块来轻松地访问所有的函数和变量,而且外部的调用者也可以更加方便地使用这些模块。因此,在Go语言中,对于一个文件中定义的模块,可以使用该文件名来作为模块名,并将其作为包的一部分进行导出。这样,调用者就可以通过 import 语句来导入该包,并在其他代码中使用该模块了。

router/enter.go 中:

package router
import "ewa_admin_server/router/system"
type RouterGroup struct {
   System system.RouterGroup
}
var RouterGroupApp = new(RouterGroup)

这段代码定义了一个名为 RouterGroup 的结构体,该结构体包含了一个名为 System 的字段,该字段的类型是 system.RouterGroup。同时,它还声明了一个名为 RouterGroupApp 的全局变量,并将其初始化为 new(RouterGroup),这意味着 RouterGroupApp 是一个指向 RouterGroup 类型的指针,并且它的值为 nil。

通常情况下,结构体是一种自定义类型,用于组合相关的字段,并将其作为单个实体来处理。在这个例子中,RouterGroup 结构体被用于定义路由分组信息,它的 System 字段会包含与路由分组相关的属性和方法。

RouterGroupApp 变量是一个全局变量,因此它可以被其他代码文件访问。该变量的用途可能是将 RouterGroup 结构体或者 System 字段在项目的其他地方进行使用。

通过创建这个结构体和全局变量,可以在应用级别上为同一类路由设置公共属性和方法,并便于在其他组件中复用,更好地对路由进行管理和组织在一个项目中。

例如,在一个 Web 应用程序中,通常会有许多不同的路由需要被注册、管理和处理。将这些路由按照业务逻辑或功能特点进行分组,可以让代码更加清晰易懂,同时也方便进行统一的权限控制、请求过滤等操作。使用结构体类型和全局变量来管理路由可以帮助开发者更好地组织代码,减少重复代码的出现,提高可维护性和可扩展性。

1.2 system 分组路由

system 分组路由主要负责一些系统层面上的路由,比如登录、注册、权限管理等。

package system
type RouterGroup struct {
   BaseRouter
}

将所有相关的路由组织在一个文件中定义 RouterGroup 结构体并导出,是为了方便其他模块或文件引入和使用。 这种方式可以避免在多个文件中重复定义结构体或变量,从而减少代码冗余和提高可读性。

另外,将所有相关的路由都在一个文件中集中定义,也可以更好地管理和维护这些路由。开发者可以根据实际需求添加、修改或删除该文件中的路由,而不必到多个文件中分别进行操作。同时,由于所有路由都在同一个文件中,也方便进行整体的统一测试和排查问题等工作。

将多个相关的路由组织在一个文件中导出,有助于减少冗余代码、提高可读性和方便管理维护,这是一种比较常见的编程实践。

1.3 BaseRouter

BaseRouter 则主要负责最基础的登录、注册等路由。比如,我们先简单写一个测试的 login 接口:

package system
import (
   "net/http"
   "github.com/gin-gonic/gin"
)
type BaseRouter struct{}
func (s *BaseRouter) InitBaseRouter(Router *gin.RouterGroup) (R gin.IRoutes) {
   baseRouter := Router.Group("base")
   {
      baseRouter.POST("login", func(context *gin.Context) {
         context.JSON(http.StatusOK, "ok")
      })
   }
   return baseRouter
}

这里面则是定义了一个 BaseRouter 结构体和它的一个 InitBaseRouter 方法。该方法接收一个 gin 的 RouterGroup 类型参数,创建一个名为 "base" 的路由组,并为其添加一个 POST 请求路由 /login。当该路由被请求时,会返回一个 JSON 格式的字符串 "ok"。

该方法返回值类型为 gin.IRoutes 接口,因此实际上返回的是创建的 baseRouter 对象,可以在其他地方使用该对象以继续往该路由组中添加更多的路由。

postman 测试一下我们刚刚写的接口:

image.png

2、自定义校验器

2.1 自定义错误信息

Gin 自带验证器返回的错误信息格式不太友好,本篇将进行调整,实现自定义错误信息,并规范接口返回的数据格式,分别为每种类型的错误定义错误码,前端可以根据对应的错误码实现后续不同的逻辑操作,篇末会使用自定义的 ValidatorResponse 实现第一个接口

utils 文件中新建 validator.go 文件,用来存放所有跟校验相关的方法:

package utils
import (
   "github.com/go-playground/validator/v10"
)
type Validator interface {
   GetMessages() ValidatorMessages
}
type ValidatorMessages map[string]string
// GetErrorMsg 获取错误信息
func GetErrorMsg(request interface{}, err error) string {
   if _, isValidatorErrors := err.(validator.ValidationErrors); isValidatorErrors {
      _, isValidator := request.(Validator)
      for _, v := range err.(validator.ValidationErrors) {
         // 若 request 结构体实现 Validator 接口即可实现自定义错误信息
         if isValidator {
            if message, exist := request.(Validator).GetMessages()[v.Field()+"."+v.Tag()]; exist {
               return message
            }
         }
         return v.Error()
      }
   }
   return "Parameter error"
}

上面定义了 GetErrorMsg 函数来获取错误信息。它接收两个参数:

  • request 参数是被验证的请求结构体。
  • err 是用 Go 自带的验证器库 validator 验证参数时返回的错误。

函数会根据不同情况返回不同的错误信息。

如果传入的 err 参数属于 Go 自带的验证器库 validatorValidationErrors 类型,即参数出现验证错误:

  • 程序会判断请求结构体是否实现了 Validator 接口。
  • 如果 request 实现了 Validator 接口,则可以自定义错误信息。这里的实现方式是:在 ValidatorMessages 中使用 <FieldName>.<Tag> 作为 key,值为对应的错误信息。例如:"name.required": "name 不能为空" 这个键值对就对应了 name 字段的 required 验证失败时输出的错误信息。
  • 如果没有实现 Validator 接口,则直接返回默认的错误信息。

最后如果参数没有验证出现错误,则返回参数错误的提示信息 "Parameter error"

然后在 model 文件中新建 system/sys_user.go


package system
import (
   "ewa_admin_server/utils"
)
type Register struct {
   Name     string `form:"name" json:"name" binding:"required"`
   Mobile   string `form:"mobile" json:"mobile" binding:"required"`
   Password string `form:"password" json:"password" binding:"required"`
}
// GetMessages 自定义错误信息
func (register Register) GetMessages() utils.ValidatorMessages {
   return utils.ValidatorMessages{
      "Name.required":     "用户名称不能为空",
      "Mobile.required":   "手机号码不能为空",
      "Password.required": "用户密码不能为空",
   }
}

接着在 rouetr/system/sys_base.go 中新建一个 register 接口:

package system
import (
   "ewa_admin_server/model/system"
   "ewa_admin_server/utils"
   "net/http"
   "github.com/gin-gonic/gin"
)
type BaseRouter struct{}
func (s *BaseRouter) InitBaseRouter(Router *gin.RouterGroup) (R gin.IRoutes) {
   baseRouter := Router.Group("base")
   {
      baseRouter.POST("login", func(context *gin.Context) {
         context.JSON(http.StatusOK, "ok")
      })
      baseRouter.POST("register", func(context *gin.Context) {
         var form system.Register
         if err := context.ShouldBindJSON(&form); err != nil {
            context.JSON(http.StatusOK, gin.H{
               "error": utils.GetErrorMsg(form, err),
            })
            return
         }
         context.JSON(http.StatusOK, gin.H{
            "message": "success",
         })
      })
   }
   return baseRouter
}

重启服务,测试一下:image.png

2.2 自定义校验器

有一些验证规则在 Gin 框架中是没有的,这个时候我们就需要自定义验证器,验证规则将统一存放在 utils/validator.go 中,新增一个校验手机号的校验器:

// ...
// ValidateMobile 校验手机号
func ValidateMobile(fl validator.FieldLevel) bool {
   mobile := fl.Field().String()
   ok, _ := regexp.MatchString(`^(13[0-9]|14[01456879]|15[0-35-9]|16[2567]|17[0-8]|18[0-9]|19[0-35-9])\d{8}$`, mobile)
   if !ok {
      return false
   }
   return true
}

initialize 中新建 other.go 文件,用来初始化一些其他的方法:

package initialize
import (
   "ewa_admin_server/utils"
   "fmt"
   "reflect"
   "strings"
   "github.com/gin-gonic/gin/binding"
   "github.com/go-playground/validator/v10"
)
func OtherInit() {
   initializeValidator()
   fmt.Println(" ===== Other init ===== ")
}
func initializeValidator() {
   if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
      // 注册自定义验证器
      _ = v.RegisterValidation("mobile", utils.ValidateMobile)
      // 注册自定义 json tag 函数
      v.RegisterTagNameFunc(func(fld reflect.StructField) string {
         name := strings.SplitN(fld.Tag.Get("json"), ",", 2)[0]
         if name == "-" {
            return ""
         }
         return name
      })
   }
}

然后在 main.go 中使用:

package main
import (
   "ewa_admin_server/core"
   "ewa_admin_server/global"
   "ewa_admin_server/initialize"
   "go.uber.org/zap"
   "github.com/gin-gonic/gin"
)
const AppMode = "debug" // 运行环境,主要有三种:debug、test、release
func main() {
   gin.SetMode(AppMode)
   // TODO:1.配置初始化
   global.EWA_VIPER = core.InitializeViper()
   // TODO:2.日志
   global.EWA_LOG = core.InitializeZap()
   zap.ReplaceGlobals(global.EWA_LOG)
   global.EWA_LOG.Info("server run success on ", zap.String("zap_log", "zap_log"))
   //  TODO:3.数据库连接
   global.EWA_DB = initialize.Gorm()
   // TODO:4.其他初始化
   initialize.OtherInit()
   // TODO:5.启动服务
   core.RunServer()
}

这样就可以在 model 中使用自定义的校验器了:

package system
import (
   "ewa_admin_server/utils"
)
type Register struct {
   Name     string `form:"name" json:"name" binding:"required"`
   Mobile   string `form:"mobile" json:"mobile" binding:"required,mobile"`
   Password string `form:"password" json:"password" binding:"required"`
}
// GetMessages 自定义错误信息
func (register Register) GetMessages() utils.ValidatorMessages {
   return utils.ValidatorMessages{
      "name.required":     "用户名称不能为空",
      "mobile.required":   "手机号码不能为空",
      "mobile.mobile":     "手机号码格式不正确",
      "password.required": "用户密码不能为空",
   }
}

测试:
image.png

3、引入 Redis

Redis(Remote Dictionary Server)是一个开源的 key-value 数据结构存储系统。它支持多种数据类型,如字符串、哈希、列表、集合、有序集合等。Redis 运行在内存中,也可以将数据存储在磁盘上,但是内存访问速度快,所以 Redis 能够提供很高的读写性能。此外,Redis 还提供了一些可扩展的功能,如发布/订阅、Lua 脚本支持和事务等。

Go 项目中引入 Redis 可以作为快速的缓存解决方案。使用 Redis 缓存可以避免每次查询数据库,从而大幅度提高应用程序的响应时间。此外,由于 Redis 原生支持多种数据类型和丰富的命令集,因此在某些情况下,Redis 还可以作为分布式锁、计数器等工具使用,以满足不同的业务需求。

3.1 Redis 配置

需要使用到一个库:

go get github.com/go-redis/redis/v8

config.yaml 中增加 redis 配置

redis: # redis 配置
  db: 0
  addr: 127.0.0.1:6379
  password: ""

config 下新建 redis.go 文件:

package config
type Redis struct {
   DB       int    `mapstructure:"db" json:"db" yaml:"db"`                   // redis的哪个数据库
   Addr     string `mapstructure:"addr" json:"addr" yaml:"addr"`             // 服务器地址:端口
   Password string `mapstructure:"password" json:"password" yaml:"password"` // 密码
}

一定要记得在 config.go 中引入:

package config
type Configuration struct {
   App   App   `mapstructure:"app" json:"app" yaml:"app"`
   Zap   Zap   `mapstructure:"zap" json:"zap" yaml:"zap"`
   MySQL MySQL `mapstructure:"mysql" json:"mysql" yaml:"mysql"`
   Pgsql PGSQL `mapstructure:"pgsql" json:"pgsql" yaml:"pgsql"`
   Redis Redis `mapstructure:"redis" json:"redis" yaml:"redis"`
}

3.2 Redis 初始化

initialize 中实现 redis 初始化方法,新增 redis.go

package initialize
import (
   "context"
   "ewa_admin_server/global"
   "fmt"
   "github.com/go-redis/redis/v8"
   "go.uber.org/zap"
)
func Redis() {
   redisCfg := global.EWA_CONFIG.Redis
   client := redis.NewClient(&redis.Options{
      Addr:     redisCfg.Addr,
      Password: redisCfg.Password, // no password set
      DB:       redisCfg.DB,       // use default DB
   })
   pong, err := client.Ping(context.Background()).Result()
   if err != nil {
      global.EWA_LOG.Error("redis connect ping failed, err:", zap.Error(err))
   } else {
      fmt.Println("====4-redis====: redis init success")
      global.EWA_LOG.Info("redis connect ping response:", zap.String("pong", pong))
      global.EWA_REDIS = client
   }
}

然后在 core/server.go 中引入:

package core
import (
   "ewa_admin_server/global"
   "ewa_admin_server/initialize"
   "fmt"
   "time"
   "go.uber.org/zap"
   "github.com/fvbock/endless"
   "github.com/gin-gonic/gin"
)
type server interface {
   ListenAndServe() error
}
func RunServer() {
   // 初始化redis服务
   initialize.Redis()
   Router := initialize.Routers()
   address := fmt.Sprintf(":%d", global.EWA_CONFIG.App.Port)
   s := initServer(address, Router)
   global.EWA_LOG.Info("server run success on ", zap.String("address", address))
   // 保证文本顺序输出
   time.Sleep(10 * time.Microsecond)
   global.EWA_LOG.Error(s.ListenAndServe().Error())
}
func initServer(address string, router *gin.Engine) server {
   // 使用endless库创建一个HTTP服务器,其中address是服务器的监听地址(如:8080),router是HTTP请求路由器。
   s := endless.NewServer(address, router)
   // 设置HTTP请求头的读取超时时间为20秒,如果在20秒内未读取到请求头,则会返回一个超时错误。
   s.ReadHeaderTimeout = 20 * time.Second
   // 设置HTTP响应体的写入超时时间为20秒,如果在20秒内未将响应体写入完成,则会返回一个超时错误。
   s.WriteTimeout = 20 * time.Second
   // 设置HTTP请求头的最大字节数为1MB。如果请求头超过1MB,则会返回一个错误。
   s.MaxHeaderBytes = 1 << 20
   return s
}

重启项目,如果你看到下面的控制台信息,说明引入成功了:image.png
至此,项目的基本架子算是成形了~

end ~

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
3月前
|
安全 前端开发 API
【Azure 应用服务】Azure Web App 服务默认支持一些 Weak TLS Ciphers Suite,是否有办法自定义修改呢?
【Azure 应用服务】Azure Web App 服务默认支持一些 Weak TLS Ciphers Suite,是否有办法自定义修改呢?
|
3月前
|
缓存 弹性计算 API
用 Go 快速开发一个 RESTful API 服务
用 Go 快速开发一个 RESTful API 服务
|
9天前
|
Go UED
Go Web服务中如何优雅平滑重启?
在生产环境中,服务升级时如何确保不中断当前请求并应用新代码是一个挑战。本文介绍了如何使用 Go 语言的 `endless` 包实现服务的优雅重启,确保在不停止服务的情况下完成无缝升级。通过示例代码和测试步骤,详细展示了 `endless` 包的工作原理和实际应用。
25 3
|
10天前
|
JSON Go UED
Go Web服务中如何优雅关机?
在构建 Web 服务时,优雅关机是一个关键的技术点,它确保服务关闭时所有正在处理的请求都能顺利完成。本文通过一个简单的 Go 语言示例,展示了如何使用 Gin 框架实现优雅关机。通过捕获系统信号和使用 `http.Server` 的 `Shutdown` 方法,我们可以在服务关闭前等待所有请求处理完毕,从而提升用户体验,避免数据丢失或不一致。
14 1
|
22天前
|
前端开发 开发者
WEB自定义页面请求响应
Web组件支持在应用拦截到页面请求后自定义响应请求能力。开发者通过onInterceptRequest()接口来实现自定义资源请求响应 。自定义请求能力可以用于开发者自定义Web页面响应、自定义文件资源响应等场景。
22 0
|
2月前
|
缓存 中间件 网络架构
Python Web开发实战:高效利用路由与中间件提升应用性能
在Python Web开发中,路由和中间件是构建高效、可扩展应用的核心组件。路由通过装饰器如`@app.route()`将HTTP请求映射到处理函数;中间件则在请求处理流程中插入自定义逻辑,如日志记录和验证。合理设计路由和中间件能显著提升应用性能和可维护性。本文以Flask为例,详细介绍如何优化路由、避免冲突、使用蓝图管理大型应用,并通过中间件实现缓存、请求验证及异常处理等功能,帮助你构建快速且健壮的Web应用。
29 1
|
2月前
|
Go API 开发者
深入探讨:使用Go语言构建高性能RESTful API服务
在本文中,我们将探索Go语言在构建高效、可靠的RESTful API服务中的独特优势。通过实际案例分析,我们将展示Go如何通过其并发模型、简洁的语法和内置的http包,成为现代后端服务开发的有力工具。
|
3月前
|
安全 Go Docker
Go服务Docker Pod不断重启排查和解决
该文章分享了Go服务在Docker Pod中不断重启的问题排查过程和解决方案,识别出并发写map导致fatal error的问题,并提供了使用sync.Map或concurrent-map库作为并发安全的替代方案。
42 4
|
3月前
|
JavaScript PHP 开发者
PHP中的异常处理与自定义错误处理器构建高效Web应用:Node.js与Express框架实战指南
【8月更文挑战第27天】在PHP编程世界中,异常处理和错误管理是代码健壮性的关键。本文将深入探讨PHP的异常处理机制,并指导你如何创建自定义错误处理器,以便优雅地管理运行时错误。我们将一起学习如何使用try-catch块捕获异常,以及如何通过set_error_handler函数定制错误响应。准备好让你的代码变得更加可靠,同时提供更友好的错误信息给最终用户。
|
3月前
|
运维 监控 程序员
Go 服务自动收集线上问题现场
Go 服务自动收集线上问题现场