添加链接
link之家
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接
+关注继续查看

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

重启服务,测试一下: 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 ~

web前端面试高频考点——Vue原理(diff算法、模板编译、组件渲染和更新、JS实现路由)
web前端面试高频考点——Vue原理(diff算法、模板编译、组件渲染和更新、JS实现路由)
彩虹女神跃长空,Go语言进阶之Go语言高性能Web框架Iris项目实战-项目入口与路由EP01
书接上回,我们已经安装好Iris框架,并且构建好了Iris项目,同时配置了fresh自动监控项目的实时编译,万事俱备,只欠东风,彩虹女神蓄势待发。现在我们来看看Iris的基础功能,如何编写项目入口文件以及配置路由系统。
【node.js从入门到精通】使用express创建web服务器,路由,进行中间件的创建链接路由及其他中间件
【node.js从入门到精通】使用express创建web服务器,路由,进行中间件的创建链接路由及其他中间件
Go Web 编程入门:HTTP 自定义路由
Go 语言提供功能丰富的 net/http,实现了基础的 HTTP 中的 client 和 server 功能。在这一篇文章也有介绍一个基础的 HelloWorld 应用。
Go 的 net/http 包为 HTTP 协议提供了很多功能。它做得不好的一件事是复杂的请求路由,例如将请求 url 分割成单个参数。 幸运的是,有一个非常流行的包,它在 Go 社区中以良好的代码质量而闻名。在此示例中,您将看到如何使用 gorilla/mux 包创建具有命名参数、GET/POST 处理程序和域限制的路由。
【零基础学Python】后端开发篇 第二十二节--Python Web开发(三):HTTP请求的url路由
【零基础学Python】后端开发篇 第二十二节--Python Web开发(三):HTTP请求的url路由
Http Server API路由请求到web程序
容器内web程序一般会绑定到http://0.0.0.0:{某监听端口}或http://+:{某监听端口},以确保使用容器IP可以访问到web应用。
Web 开发框架 — Express 精讲(安装使用、静态托管、路由处理、中间件的使用)(2)
Web 开发框架 — Express 精讲(安装使用、静态托管、路由处理、中间件的使用)(2)