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

本文涉及的产品
云数据库 Redis 版,社区版 2GB
推荐场景:
搭建游戏排行榜
云数据库 RDS MySQL Serverless,0.5-2RCU 50GB
云数据库 RDS MySQL Serverless,价值2615元额度,1个月
简介: 【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
相关文章
|
12天前
|
存储 缓存 NoSQL
【Go语言专栏】Go语言中的Redis操作与缓存应用
【4月更文挑战第30天】本文探讨了在Go语言中使用Redis进行操作和缓存应用的方法。文章介绍了Redis作为高性能键值存储系统,用于提升应用性能。推荐使用`go-redis/redis`库,示例代码展示了连接、设置、获取和删除键值对的基本操作。文章还详细阐述了缓存应用的步骤及常见缓存策略,包括缓存穿透、缓存击穿和缓存雪崩的解决方案。利用Redis和合适策略可有效优化应用性能。
|
1天前
|
Go
golang学习3,golang 项目中配置gin的web框架
golang学习3,golang 项目中配置gin的web框架
|
2天前
|
jenkins 持续交付
基于Jeecgboot前后端分离的平台后端系统采用jenkins发布
基于Jeecgboot前后端分离的平台后端系统采用jenkins发布
|
7天前
|
存储 NoSQL 测试技术
Redis数据存储系统为什么快?
Redis的快速并非偶然,而是深思熟虑的设计理念的结果。通过将数据存储于内存、采用单线程模型、实现非阻塞I/O等独特的技术选择,Redis在高并发和低延迟方面展现了卓越的表现。
34 16
|
12天前
|
NoSQL Shell Go
在go中简单使用go-redis库
在go中简单使用go-redis库
|
12天前
|
缓存 监控 测试技术
【Go语言专栏】使用Go语言构建高性能Web服务
【4月更文挑战第30天】本文探讨了使用Go语言构建高性能Web服务的策略,包括Go语言在并发处理和内存管理上的优势、基本原则(如保持简单、缓存和并发控制)、标准库与第三方框架的选择、编写高效的HTTP处理器、数据库优化以及性能测试和监控。通过遵循最佳实践,开发者可以充分利用Go语言的特性,构建出高性能的Web服务。
|
12天前
|
中间件 Go API
【Go 语言专栏】Go 语言中的 Web 框架比较与选择
【4月更文挑战第30天】本文对比了Go语言中的四个常见Web框架:功能全面的Beego、轻量级高性能的Gin、简洁高效的Echo,以及各自的性能、功能特性、社区支持。选择框架时需考虑项目需求、性能要求、团队经验和社区生态。开发者应根据具体情况进行权衡,以找到最适合的框架。
|
12天前
|
缓存 NoSQL Java
【亮剑】分布式锁是保证多服务实例同步的关键机制,常用于互斥访问共享资源、控制访问顺序和系统保护,如何使用注解来实现 Redis 分布式锁的功能?
【4月更文挑战第30天】分布式锁是保证多服务实例同步的关键机制,常用于互斥访问共享资源、控制访问顺序和系统保护。基于 Redis 的分布式锁利用 SETNX 或 SET 命令实现,并考虑自动过期、可重入及原子性以确保可靠性。在 Java Spring Boot 中,可通过 `@EnableCaching`、`@Cacheable` 和 `@CacheEvict` 注解轻松实现 Redis 分布式锁功能。
|
12天前
|
API 开发者 UED
构建高效微服务架构:后端开发的新趋势移动应用与系统:开发与优化的艺术
【4月更文挑战第30天】 随着现代软件系统对可伸缩性、灵活性和敏捷性的日益需求,传统的单体应用架构正逐渐向微服务架构转变。本文将探讨微服务架构的核心概念,分析其优势,并着重讨论如何利用最新的后端技术栈实现一个高效的微服务系统。我们将涵盖设计模式、服务划分、数据一致性、服务发现与注册、API网关以及容器化等关键技术点,为后端开发者提供一份实操指南。 【4月更文挑战第30天】 在数字化时代的浪潮中,移动应用和操作系统的紧密交织已成为日常生活和商业活动的基石。本文将深入探讨移动应用开发的关键技术、跨平台开发工具的选择以及移动操作系统的架构和性能优化策略。通过分析当前移动应用开发的挑战与机遇,我们将
|
13天前
|
机器学习/深度学习 Kubernetes 微服务
后端技术发展及其在高性能系统中的应用研究
后端技术发展及其在高性能系统中的应用研究
18 0