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

本文涉及的产品
云数据库 Redis 版,社区版 2GB
推荐场景:
搭建游戏排行榜
云数据库 RDS MySQL Serverless,0.5-2RCU 50GB
简介: 【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月前
|
运维 网络协议 安全
长连接网关技术专题(十):百度基于Go的千万级统一长连接服务架构实践
本文将介绍百度基于golang实现的统一长连接服务,从统一长连接功能实现和性能优化等角度,描述了其在设计、开发和维护过程中面临的问题和挑战,并重点介绍了解决相关问题和挑战的方案和实践经验。
84 1
|
1月前
|
负载均衡 Java 中间件
使用Go语言构建高性能Web服务
Go语言作为一种快速、高效的编程语言,其在构建高性能Web服务方面具有独特优势。本文将探讨如何利用Go语言开发和优化Web服务,以实现更高的性能和可伸缩性。
|
2月前
|
SQL 分布式计算 Hadoop
Azkaban【基础 01】核心概念+特点+Web界面+架构+Job类型(一篇即可入门Azkaban工作流调度系统)
【2月更文挑战第6天】Azkaban【基础 01】核心概念+特点+Web界面+架构+Job类型(一篇即可入门Azkaban工作流调度系统)
101 0
|
1月前
|
SQL 存储 数据库
基于Web技术的在线考试系统的设计与实现(论文+源码)_kaic
基于Web技术的在线考试系统的设计与实现(论文+源码)_kaic
|
23天前
|
资源调度 JavaScript 安全
Linux系统之部署web-check网站分析工具
【4月更文挑战第3天】Linux系统之部署web-check网站分析工具
68 9
|
26天前
|
Java
销售业务管理系统【Web系统】(Java课设)
销售业务管理系统【Web系统】(Java课设)
13 5
|
1月前
|
SQL 机器学习/深度学习 缓存
Go语言Web应用实战与案例分析
【2月更文挑战第21天】本文将通过实战案例的方式,深入探讨Go语言在Web应用开发中的应用。我们将分析一个实际项目的开发过程,展示Go语言在构建高性能、可扩展Web应用方面的优势,并分享在开发过程中遇到的问题和解决方案,为读者提供宝贵的实战经验。
|
1月前
|
安全 中间件 Go
Go语言Web服务性能优化与安全实践
【2月更文挑战第21天】本文将深入探讨Go语言在Web服务性能优化与安全实践方面的应用。通过介绍性能优化策略、并发编程模型以及安全加固措施,帮助读者理解并提升Go语言Web服务的性能表现与安全防护能力。
|
1月前
|
开发框架 JSON Go
Go语言Web开发基础与框架探索
【2月更文挑战第21天】本文将带领读者深入了解Go语言在Web开发领域的基础知识和常用框架。通过介绍Go语言的Web开发特点、核心库的使用,以及流行框架如Gin、Echo等的基本用法和优势,帮助读者快速上手Go语言Web开发,提升开发效率。
|
1月前
|
前端开发 NoSQL Java
【SpringBoot】秒杀业务:redis+拦截器+自定义注解+验证码简单实现限流
【SpringBoot】秒杀业务:redis+拦截器+自定义注解+验证码简单实现限流