本文正在参加「金石计划」
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
来管理:
在 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
测试一下我们刚刚写的接口:
2、自定义校验器
2.1 自定义错误信息
Gin
自带验证器返回的错误信息格式不太友好,本篇将进行调整,实现自定义错误信息,并规范接口返回的数据格式,分别为每种类型的错误定义错误码,前端可以根据对应的错误码实现后续不同的逻辑操作,篇末会使用自定义的 Validator
和 Response
实现第一个接口
在 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 自带的验证器库 validator
的 ValidationErrors
类型,即参数出现验证错误:
- 程序会判断请求结构体是否实现了
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 }
重启服务,测试一下:
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": "用户密码不能为空", } }
测试:
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 }
重启项目,如果你看到下面的控制台信息,说明引入成功了:
至此,项目的基本架子算是成形了~
end ~