后端笔记之gin框架学习

发布时间:2023年12月20日

1. 使用脚手架搭建gin框架

gin框架推荐使用 go mod 来管理依赖,所以使用 go mod 获取 gin

在 $GOPATH/src 下新建一个 gin(项目包名,自定义)文件夹,进入其中后,执行命令: go get -u github.com/gin-gonic/gin 即可自动下载依赖,等到依赖下载完成之后,在当前目录执行 go mod tidy 即可检查依赖完整性。

至此一个 gin 框架就搭建完成了。

2. 应用框架

在 gin 文件夹下建立一个 main.go 文件,编写 main 函数引入 gin 引擎。

        package main
    	import (
        	"github.com/gin-gonic/gin"
        )
    
        func main() {
        	// 获取 gin 引擎
            ginEngine := gin.Default()
            // 设置一个 get 路由,以及 handle 方式
            ginEngine.GET("/ping", func(c *gin.Context) {
                c.JSON(200, gin.H{
                    "message": "pong",
                })
        	})
            // 运行 gin 进程, 默认 8080 端口
            ginEngine.Run()
        }

至此,这个框架已经可以启动了。

3. 路由管理

在 gin 目录下新建一个包,包名通常为 router

router 下的一个 go 文件通常代表一个路由分组,例如下面是个无权限白名单的接口分组示例:

新建 public.go 文件

        package router
    	import (
            "gin/web"
            "github.com/gin-gonic/gin"
        )
    
        func InitPublicApi(ginEngine *gin.Engine) {
            // 分组, ginEngine的Group是设置根目录 api
            api := ginEngine.Group("/api")
            // 设置次级目录 v1
            v1 := api.Group("/v1")
            // 设置 v1 下的子路径,假使这个路径是获取验证码,第二个参数就是具体的处理方式
            v1.GET("/verificationCode", web.VerificationCode)
            // 设置一个登录的 POST 路径
            v1.POST("/login", web.Login)
        }

同时 gin 目录下新建 web 文件夹,内部搭配新建 public.go 文件,然后用于处理路由指引过来的业务逻辑

        package web
    
        import (
            "fmt"
            "github.com/gin-gonic/gin"
        )
    
        func VerificationCode(context *gin.Context) {
            fmt.Println("生成了一个验证码")
        }
    
        func Login(context *gin.Context) {
            fmt.Println("处理了登录逻辑")
        }

在 router 下新建 routers.go 文件,在这里把所有的路由都创建出来

        package router
    
        import "github.com/gin-gonic/gin"
    
        func InitRouters(ginEngine *gin.Engine) {
            InitPublicApi(ginEngine)
            InitUserApi(ginEngine)
        }

然后在 main 函数中调用即可

        package main
    	import (
        	"github.com/gin-gonic/gin"
        )
    
        func main() {
        	// 获取 gin 引擎
            ginEngine := gin.Default()
            // 创建所有的路由
            router.InitRouters(ginEngine)
            // 运行 gin 进程, 默认 8080 端口,但是可以修改为任意的空闲端口,以下改变为了8081
            ginEngine.Run(":8081")
        }

4.自定义中间件的使用

在gin框架中,我们所有要对公共的处理都可以使用中间件来实现,所谓的中间件就是通过函数作为参数在完成本函数之前去额外做一些操作,由于。

首先现在gin目录下新建middleware文件夹,然后新建 myHandler.go 文件。

        package middleware
        import (
            "fmt"
            "github.com/gin-gonic/gin"
        )
    
        // MyHandler 这边自定义一个中间件,打印请求路径和方式
        func MyHandler() gin.HandlerFunc {
            // 返回一个匿名函数
            return func(context *gin.Context) {
                path := context.FullPath()       // 获取完整的请求连接
                method := context.Request.Method // 获取请求方式类型
                fmt.Printf("myHandler, path:%s, method: %s\n", path, method)
                context.Next()
            }
        }

然后再 main 函数中引入使用

       func main() {
            // 获取 gin 引擎
            ginEngine := gin.Default()
            // 中间件使用,用ginEngine.use
            ginEngine.Use(middleware.MyHandler())
            // 创建整个路由
            router.InitRouters(ginEngine)
            // 运行 gin 进程, 默认 8080 端口, 可以改变为任意空闲端口
            ginEngine.Run(":8081")
        }

当我们再次请求后台时,路径和方式就被打印出来了
在这里插入图片描述

5. 通过中间件设置路由权限校验

1. 自定义校验

在 middleware 文件夹下新建 token.go 文件,用来作为校验 token 的中间件

        package middleware
    
        import (
            "errors"
            "github.com/gin-gonic/gin"
            "net/http"
        )
    
        var token = "123456"
    
        func TokenCheck(context *gin.Context) {
            // 从请求头中获取token
            accessToken := context.GetHeader("access_token")
            // 进行token校验,在正规流程中,这里应该拿着token做完加解密之后,通过jwt获取存于缓存或者redis中的用户信息,并把用户信息写入请求之中
            // 这里模拟上述操作,仅仅是比较一下token后,塞入用户信息
            if accessToken != token {
                context.JSON(http.StatusInternalServerError, gin.H{
                    "message": "token 校验失败",
                })
                // 防止此处理继续下去
                // context.Abort()
                // 防止此处理继续下去并报错
                context.AbortWithError(http.StatusInternalServerError, errors.New("token checker fail"))
            }
            // 如果校验成功之后,那么将要通过jwt根绝token获取到用户信息,加入请求中
            context.Set("userId", "123456789")
            context.Set("userName", "sue")
            context.Next()
        }

然后再需要使用 token 校验的路由上配置

        package router
    
        import (
            "fmt"
            "gin/middleware"
            "github.com/gin-gonic/gin"
        )
    
        func InitUserApi(ginEngine *gin.Engine) {
            // 在这里配置上需要校验权限的路由的中间件
            user := ginEngine.Group("/user", middleware.TokenCheck)
            v1 := user.Group("/v1")
    
            //路径传参
            v1.GET("/detail/:id", func(context *gin.Context) {
                // 可以通过 Param 获取到参数值
                id := context.Param("id")
                // 在这里获取中间件中塞入的信息
                userId = context.getString("userId")
                fmt.Println(userId)
                context.String(200, "ID is %s", id)
            })
    
        }

2. 配置跨域

使用 go get -u github.com/gin-contrib/cors下载跨域依赖包,然后编写中间件

    func Cors() gin.HandlerFunc {
        return cors.New{
            AllowAllOrigins: true,
            AllowHeaders: []string{
                "Origin", "Content-Length", "Content-Type",
            },
            AllowMethods: []string{
                "GET", "POST", "DELETE", "PUT", "HEAD", "OPTIONS"
            }
        }
    }

然后再 router 最根部使用

     	package router
    
    	import (
        	"github.com/gin-gonic/gin"
            "gin/middleware"
    
        func InitRouters(ginEngine *gin.Engine) {
            api := ginEngine.Group("/api")
            api.Use(middleware.Cors())
            InitPublicApi(api)
            InitUserApi(api)
        }

3. 使用jwt进行tokn校验

先要去下载 jwt 依赖,使用go get -u github.com/golang-jwt/jwt/v5下载。

之后中间件包下,新建 jwt.go

        package middleware
    
        import "github.com/golang-jwt/jwt/v5"
    
        // 秘钥
        var key = "abcdefg123456"
    
        type Data struct {
            Id                   string
            Name                 string
            Age                  int
            Sex                  int
            jwt.RegisteredClaims // 这里就是使用鸭子模式,自动隐式判断继承
        }
    
        // Sign 签发token
        func Sign(data jwt.Claims) (string, error) {
            // 选择加密方式签发token
            token := jwt.NewWithClaims(jwt.SigningMethodHS256, data)
            // 签名转化为字符串
            sign, err := token.SignedString([]byte(key))
            if err != nil {
                return "", err
            }
            return sign, err
        }
    
        // Verify 验签
        func Verify(sign string, data jwt.Claims) error {
            _, err := jwt.ParseWithClaims(sign, data, func(token *jwt.Token) (interface{}, error) {
                return []byte(key), nil
            })
            return err
        }

然后修改原来的token验证方法

       package middleware
    
        import (
            "errors"
            "github.com/gin-gonic/gin"
            "net/http"
        )
    
    
        func TokenCheck(context *gin.Context) {
            // 从请求头中获取token
            accessToken := context.GetHeader("access_token")
            // 进行token校验,在正规流程中,这里应该拿着token做完加解密之后,通过jwt获取存于缓存或者redis中的用户信息,并把用户信息写入请求之中
            data := &Data{}
            // 调用验签方法
            err := Verify(accessToken, data)
            if err != nil {
                context.JSON(http.StatusInternalServerError, gin.H{
                    "message": "token 校验失败",
                })
                // 防止此处理继续下去
                // context.Abort()
                // 防止此处理继续下去并报错
                context.AbortWithError(http.StatusInternalServerError, errors.New("token checker fail"))
            }
            // 把用户信息塞到这次请求的上下文中
            context.Set("user", data)
            context.Next()
        }

之后,修改登录方法

        func Login(context *gin.Context) {
            req := &loginReq{}
            err := context.ShouldBindJSON(req)
            if err != nil {
                context.JSON(http.StatusInternalServerError, gin.H{
                    "message": err.Error(),
                })
                return
            }
            // 在这里应该通过name 和 password 去数据库取到用户信息,然后填充进签名信息中
            data := middleware.Data{
                Id:   "123",
                Name: req.Name,
                Sex:  1,
                Age:  18,
                RegisteredClaims: jwt.RegisteredClaims{
                    // 有效期为一个小时
                    ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour)),
                    // 签发时间
                    IssuedAt: jwt.NewNumericDate(time.Now()),
                    // 有效期的开始时间
                    NotBefore: jwt.NewNumericDate(time.Now()),
                },
            }
            sign, err := middleware.Sign(data)
            if err != nil {
                context.JSON(http.StatusInternalServerError, gin.H{
                    "message": err,
                })
                return
            }
            context.JSON(http.StatusOK, gin.H{
                "token": sign,
            })
        }

最后,先调用登录信息拿到 token 之后放入请求头的 access_token 中,调用获取当前登录人的信息的接口,代码如下:

  	v1.GET("/detail", func(context *gin.Context) {
            // 从上下文中获取在token中间件中塞入的用户信息
    		if userData, exist := context.Get("user"); exist {
                // 返回给前端
    			context.JSON(http.StatusOK, userData)
    			return
    		}
    		context.JSON(http.StatusInternalServerError, gin.H{
    			"message": "用户不存在",
    		})
    	})

6. 接口入参获取和绑定

  1. 入参获取

  2. 在restful风格接口里面传参,比如像/xxx/xxx/500,在代码中路由地址中声明为”/xxx/xxx/:id“,那么可以使用Param获取

       v1.GET("/detail/:id", func(context *gin.Context) {
       		// 可以通过 Param 获取到参数值
       		id := context.Param("id")
       		context.String(200, "ID is %s", id)
       	})
  1. 在传统的url传参的形式下,如/xxx/xxx?id=500,那么可以使用Query获取
      	v1.GET("/detail?id=500", func(context *gin.Context) {
       		// 可以通过 Param 获取到参数值
       		id := context.Query("id")
       		context.String(200, "ID is %s", id)
       	})
  1. 在 form 表单提交的时候,使用结构体去承接入参
    在 web 包中的逻辑处理代码如下:
 			type loginReq struct {
	             name     string `form:"nickName" json:"nickName"` // 反括号内中 form是表名提交方式, nickName是表名提交的数据的key
	             password string `form:"password" json:"password"` // 那么对应的json提交的时候,这里就是 `json:"password"`,前面的属性也应该首字母大写,JSON会自动转化为小写
			 }
   
           func Login(context *gin.Context) {
               // 声明承接的结构体实例
               req := &loginReq{}
               // shouldBind 为绑定提交,基本上可以应对所有的方式提交,json提交时也可以指明为使用ShouldBindJSON
               // Bind 和 ShouldBind区别在于使用 Bind 当入参不满足格式时会返回400错误,而不是继续向下执行进入代码中设定的错误。
               err := context.ShouldBind(req)
               if err != nil {
                   context.JSON(http.StatusInternalServerError, gin.H{
                       "message": err.Error(),
                   })
                   return
               }
               fmt.Println(req)
               context.JSON(http.StatusOK, req)
           }

请求和返回如下图所示:
在这里插入图片描述

2. 参数校验

验证框架是:validator

几个校验示例:

    type loginReq struct {
        Name     string `json:"nickName" binding: "required"` // 加入binding: "required"是验证必填
    	Password string `json:"password"`
        Phone    string `json:"password binding: "required,el64"` // el64是验证电话格式
        Email    string `json:"password binding: "omitempty,email"` // omitempty为当前值为空,就不在进行后续校验,email是验证邮箱格式
    }

3. protobuf

Protobuf(Protocol Buffers)是一种由 Google 开发的数据序列化格式和编程语言无关的接口定义语言(IDL,Interface Definition Language)。它的主要目的是用于定义数据结构和消息格式,以便在不同平台和不同语言之间进行高效的数据交换。

使用 Protobuf,你可以定义结构化数据的消息格式,并生成针对多种编程语言的序列化和反序列化代码。这使得不同系统之间可以相互通信、交换数据,而无需关心底层的数据表示和传输细节。

Protobuf 提供了一种简洁、高效的二进制编码格式,它比 XML 和 JSON 等文本格式更紧凑,解析速度更快,同时也更容易进行版本升级和兼容性处理。因此,Protobuf 在诸如分布式系统通信、数据存储和数据交换等领域具有广泛的应用。

使用 Protobuf 的基本流程如下:

  1. 使用 Protobuf 的语法定义消息类型和结构化数据的字段。
  2. 使用 Protobuf 编译器将定义的 Protobuf 文件(通常以 .proto 为后缀)转换为目标语言的代码文件。
  3. 在程序中使用生成的代码来序列化和反序列化消息。

总结起来,Protobuf 提供了一种跨语言、跨平台的数据序列化方案,使得不同系统之间可以高效、可靠地进行数据交换和通信。

7. 集成mysql数据库

1. gorm使用

gorm 是 golang 对于 sql 和对象的一种关系映射的框架,orm因为牵扯到映射,所以不推荐用于多表连接的操作。

使用go get -u gorm.io/gorm 下载依赖,以及go get -u gorm.io/driver/mysql 下mysql驱动。文档地址

  1. 创建连接池
    然后在 middleware 文件夹下面新建 grom.go 文件
 	   package middleware
       
       import (
       	"gorm.io/driver/mysql"
       	"gorm.io/gorm"
       	"gorm.io/gorm/logger"
       	"log"
       	"time"
       )
       
       var DB *gorm.DB
       
       var dsn = "admin:pass12345sx@tcp(127.0.0.1:3306)/hit-plum?charset=utf8mb4&parseTime=True&loc=Local"
       
       func init() {
       	var err error
       	DB, err = gorm.Open(mysql.New(mysql.Config{
       		DSN:                       dsn,   // data source name
       		DefaultStringSize:         256,   // 默认字符配置
       		DisableDatetimePrecision:  true,  // 禁用日期时间精度,MySQL 5.6之前不支持此功能
       		DontSupportRenameIndex:    true,  // 重命名索引时删除并创建,MySQL 5.7之前不支持重命名索引,MariaDB
       		DontSupportRenameColumn:   true,  // `change`重命名列时,MySQL 8、MariaDB之前不支持重命名列
       		SkipInitializeWithVersion: false, // 基于当前MySQL版本自动配置
       	}), &gorm.Config{
       		Logger: logger.Default.LogMode(logger.Info), // 设置一下log的日志级别
       	})
       	if err != nil {
       		log.Println(err)
       		return
       	}
       	setPool(DB)
       }
       
       // setPool 设置连接池
       func setPool(db *gorm.DB) {
       	sqlDB, err := db.DB()
       	if err != nil {
       		log.Println(err)
       		return
       	}
       	sqlDB.SetMaxIdleConns(5)                // 最大空闲连接数,即最大可以长期保持的连接数,超过这个数的链接会按照策略自动关闭
       	sqlDB.SetMaxIdleConns(10)               // 最大同时连接数
       	sqlDB.SetConnMaxLifetime(time.Hour / 2) // 设置最大存活周期为半小时,超过这个周期还存在链接将会被回收
       
       }
  1. 新增model
    在 web 包下新建 models 包,新增 user.go 文件
       package models
       
       import (
       	"time"
           "gorm.io/gorm"
       )
       // 共有的字段
       type Model struct {
       	gorm.Model // gorm 提供的共有字段
       	UpdatedBy string
       	CreatedBy string
       	DeletedBy string
       }
       
       type User struct {
       	Model     Model  `gorm:"embedded"` // 嵌入共有的一些字段
       	UserName  string `gorm:"column:user_name"` // 声明对应的列名
       	Password  string
       	RealName  string
       	UserType  uint
       	Email     string
       	Phone     string
       	DepartId  int
       	Gender    uint
       	Avatar    string `gorm:"type:text"` // 声明对应的字段数据库数据类型
       	Enabled   uint
       	DelFlag   uint
       	LoginIp   string
       	LoginDate time.Time
       	Remark    string `gorm:"type:text"`
       }
       // TableName 方法指定表名, 默认是根据 结构体名字后加个s为表名,不符合此规范就要单独声明表名
       func (User) TableName() string {
       	return "user"
       }
  1. 增删改查
    web 下新增一个 dao 包,然后建立一个 user.go 文件
// 用于测试的数据对象
       var userTempData = models.User{
       	UserName:  "sue",
       	Password:  "123456",
       	RealName:  "ElvisSue",
       	UserType:  1,
       	Email:     "ElvisSue@163.com",
       	Phone:     "15066514789",
       	DepartId:  456,
       	Gender:    1,
       	Avatar:    "",
       	Enabled:   0,
       	DelFlag:   1,
       	LoginIp:   "localhost",
       	LoginDate: time.Now(),
       	Remark:    "this is a handsome man",
       	Model: models.Model{
       		CreatedBy: "admin",
       		UpdatedBy: "",
       		DeletedBy: "",
       	},
       }

增:

       // CreateUser 新增一条数据
       func CreateUser() {
       	t := userTempData
       	res := middleware.DB.Create(&t)
       	fmt.Println(res.RowsAffected)
       	fmt.Println(res.Error)
       	fmt.Println(t)
       }
       // PartialCreateUser 部分插入
       func PartialCreateUser() {
       	t := userTempData
       	res := middleware.DB.Select("UserName", "Password", "CreatedBy").Create(&t)
       	fmt.Println(res.RowsAffected)
       	fmt.Println(res.Error)
       	fmt.Println(t)
       }
       // OmitCreateUser 忽略某些字段插入
       func OmitCreateUser() {
       	t := userTempData
       	res := middleware.DB.Omit("LoginIp").Create(&t)
       	fmt.Println(res.RowsAffected)
       	fmt.Println(res.Error)
       	fmt.Println(t)
       }
       // BatchCreateUser 批量插入
       func BatchCreateUser() {
       	users := make([]models.User, 8)
       	for i := 0; i < 3; i++ {
       		t := userTempData
       		users = append(users, t)
       	}
       	res := middleware.DB.Create(&users)
       	fmt.Println(res.RowsAffected)
       	fmt.Println(res.Error)
       }

删:其实现在大部分数据采用的是逻辑删除,删这个操作并不是特别重要,所以一个接口演示出所有的情况

       // DeleteUser 删除用户
       func DeleteUser(){
           temp := models.User{
               id: 10
           }
           middleware.DB.Delete(&temp) // 删除主键为10的
           middleware.DB.Delete(&models.User{}, 10) // 删除主键为10的
           middleware.DB.Delete(&models.User{}, []int{1,2,3})// 删除主键 in [1, 2, 3]的
           middleware.DB.Where("user_name = ?", "jinzhu").Delete(&temp) // 删除主键为10的且 user_name 为 “jinzhu” 的
           middleware.DB.Delete(&models.User{},"user_name LIKE ?", "%jinzhu%") // 删除主键为10的且 user_name 包含 “jinzhu” 的
       }

改:
// Save 这个接口有些特殊,在未声明where条件且要传入的model不含有主键的情况下,会默认使用 insert 去更新表数据,设置 where 条件后或者 model 中有已存在的主键才会使用 update 更新表数据

        var temp = models.User{
               ID: 8
               UserName: "lee",
               Password: "153654"
           }
       // SaveUser 修改一条数据
       func SaveUser() {
           t := temp
           middleware.DB.Save(&t)// 因为ID是主键,更改了 ID 为8的数据
       }
       // SaveSingleColumn 修改单一列
       func SaveSingleColumn (){
            t := temp
           // 使用空的结构体来声明表名进行操作
           middleware.DB.Model(&models.User{}).Where("enabled = ?", 1).Update("UserName", "hello")
           // 使用已有主键的model进行操作, 即 where id = x
           middleware.DB.Model(&userTempData).Update("UserName", "hello")
            // 使用已有主键的model进行操作加where, 即 where id = x and enabled = 1
           middleware.DB.Model(&userTempData).Where("enabled = ?", 1).Update("UserName", "hello")
       }
       // SaveMultipleColumn 多列更改
       func SaveMultipleColumn (){
            t := temp
            // 当满足 where id = 5 and enabled = 1 的时候,t 中有的属性都会更改
            middleware.DB.Model(&models.User{ID: 5}).Where("Enabled = ?", 1).Updates(&t)
       }
       // SaveSelectUser 选择列修改,其实和多列修改差不多
       func SaveSelectUser(){
           // 满足 where id = 5 只修改 user_name 的值
           middleware.DB.Model(&models.User{ID: 5}).Select("UserName").Updates(&t)
       }

查:

       func QueryUser() {
           var user models.User
           var users []models.User
           middleware.DB.Model(&models.User{}).First(&user) // 按照id排序去第一条
           middleware.DB.Model(&models.User{}).Take(&user) // 默认第一条
           middleware.DB.Model(&models.User{}).Last(&user) // id 排序最后一条
           middleware.DB.Model(&models.User{}).First(&user, 8) // id = 8的
           middleware.DB.Model(&models.User{}).First(&user,"id = ?",  8) // id = 8的
           middleware.DB.Model(&models.User{}).First(&user, []int{1, 2, 3})// id in [1,2,3]的
           middleware.DB.Where("user_name <> ?", "jinzhu").Find(&users) // 所有 user_name <> 'jinzhu'
       }
文章来源:https://blog.csdn.net/qq_41910359/article/details/135079510
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。