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

本文涉及的产品
云数据库 Tair(兼容Redis),内存型 2GB
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
简介: 【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
相关文章
|
1月前
|
存储 开发工具 数据库
认证源码分析与自定义后端认证逻辑
认证源码分析与自定义后端认证逻辑
35 0
认证源码分析与自定义后端认证逻辑
|
7天前
|
机器学习/深度学习 前端开发 算法
婚恋交友系统平台 相亲交友平台系统 婚恋交友系统APP 婚恋系统源码 婚恋交友平台开发流程 婚恋交友系统架构设计 婚恋交友系统前端/后端开发 婚恋交友系统匹配推荐算法优化
婚恋交友系统平台通过线上互动帮助单身男女找到合适伴侣,提供用户注册、个人资料填写、匹配推荐、实时聊天、社区互动等功能。开发流程包括需求分析、技术选型、系统架构设计、功能实现、测试优化和上线运维。匹配推荐算法优化是核心,通过用户行为数据分析和机器学习提高匹配准确性。
34 3
|
20天前
|
机器学习/深度学习 人工智能 算法
【AI系统】AI 编译器后端优化
AI编译器采用多层架构,首先通过前端优化将不同框架的模型转化为统一的Graph IR并进行计算图级别的优化,如图算融合、内存优化等。接着,通过后端优化,将优化后的计算图转换为TensorIR,针对单个算子进行具体实现优化,包括循环优化、算子融合等,以适应不同的硬件架构,最终生成高效执行的机器代码。后端优化是提升算子性能的关键步骤,涉及复杂的优化策略和技术。
39 3
|
23天前
|
机器学习/深度学习 人工智能 算法
【AI系统】LLVM 后端代码生成
本文介绍 LLVM 后端的代码生成过程,包括将优化后的 LLVM IR 转换为目标代码的关键步骤,如指令选择、寄存器分配、指令调度等,以及后端如何支持不同硬件平台的代码生成。
30 6
|
1月前
|
缓存 监控 前端开发
在 Go 语言中实现 WebSocket 实时通信的应用,包括 WebSocket 的简介、Go 语言的优势、基本实现步骤、应用案例、注意事项及性能优化策略,旨在帮助开发者构建高效稳定的实时通信系统
本文深入探讨了在 Go 语言中实现 WebSocket 实时通信的应用,包括 WebSocket 的简介、Go 语言的优势、基本实现步骤、应用案例、注意事项及性能优化策略,旨在帮助开发者构建高效稳定的实时通信系统。
95 1
|
1月前
|
中间件 Go API
Go语言中几种流行的Web框架,如Beego、Gin和Echo,分析了它们的特点、性能及适用场景,并讨论了如何根据项目需求、性能要求、团队经验和社区支持等因素选择最合适的框架
本文概述了Go语言中几种流行的Web框架,如Beego、Gin和Echo,分析了它们的特点、性能及适用场景,并讨论了如何根据项目需求、性能要求、团队经验和社区支持等因素选择最合适的框架。
80 1
|
1月前
|
数据库连接 Go 数据库
Go语言中的错误注入与防御编程。错误注入通过模拟网络故障、数据库错误等,测试系统稳定性
本文探讨了Go语言中的错误注入与防御编程。错误注入通过模拟网络故障、数据库错误等,测试系统稳定性;防御编程则强调在编码时考虑各种错误情况,确保程序健壮性。文章详细介绍了这两种技术在Go语言中的实现方法及其重要性,旨在提升软件质量和可靠性。
32 1
|
1月前
|
Go UED
Go Web服务中如何优雅平滑重启?
在生产环境中,服务升级时如何确保不中断当前请求并应用新代码是一个挑战。本文介绍了如何使用 Go 语言的 `endless` 包实现服务的优雅重启,确保在不停止服务的情况下完成无缝升级。通过示例代码和测试步骤,详细展示了 `endless` 包的工作原理和实际应用。
50 3
|
1月前
|
JSON Go UED
Go Web服务中如何优雅关机?
在构建 Web 服务时,优雅关机是一个关键的技术点,它确保服务关闭时所有正在处理的请求都能顺利完成。本文通过一个简单的 Go 语言示例,展示了如何使用 Gin 框架实现优雅关机。通过捕获系统信号和使用 `http.Server` 的 `Shutdown` 方法,我们可以在服务关闭前等待所有请求处理完毕,从而提升用户体验,避免数据丢失或不一致。
26 1
|
1月前
|
缓存 前端开发 中间件
go语言中Web框架
【10月更文挑战第22天】
46 4