gin框架学习
-
- [1. 使用脚手架搭建gin框架](#1. 使用脚手架搭建gin框架)
- [2. 应用框架](#2. 应用框架)
- [3. 路由管理](#3. 路由管理)
- 4.自定义中间件的使用
- [5. 通过中间件设置路由权限校验](#5. 通过中间件设置路由权限校验)
-
- [1. 自定义校验](#1. 自定义校验)
- [2. 配置跨域](#2. 配置跨域)
- [3. 使用jwt进行tokn校验](#3. 使用jwt进行tokn校验)
- [6. 接口入参获取和绑定](#6. 接口入参获取和绑定)
-
- [2. 参数校验](#2. 参数校验)
- [3. protobuf](#3. protobuf)
- [7. 集成mysql数据库](#7. 集成mysql数据库)
-
- [1. gorm使用](#1. gorm使用)
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 引擎。
go
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 文件
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 文件,然后用于处理路由指引过来的业务逻辑
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 文件,在这里把所有的路由都创建出来
go
package router
import "github.com/gin-gonic/gin"
func InitRouters(ginEngine *gin.Engine) {
InitPublicApi(ginEngine)
InitUserApi(ginEngine)
}
然后在 main 函数中调用即可
go
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 文件。
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 函数中引入使用
go
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 的中间件
go
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 校验的路由上配置
go
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下载跨域依赖包,然后编写中间件
go
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 最根部使用
go
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
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验证方法
go
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()
}
之后,修改登录方法
go
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 中,调用获取当前登录人的信息的接口,代码如下:
go
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. 接口入参获取和绑定
-
入参获取
-
在restful风格接口里面传参,比如像/xxx/xxx/500,在代码中路由地址中声明为"/xxx/xxx/:id",那么可以使用Param获取
go
v1.GET("/detail/:id", func(context *gin.Context) {
// 可以通过 Param 获取到参数值
id := context.Param("id")
context.String(200, "ID is %s", id)
})
- 在传统的url传参的形式下,如/xxx/xxx?id=500,那么可以使用Query获取
go
v1.GET("/detail?id=500", func(context *gin.Context) {
// 可以通过 Param 获取到参数值
id := context.Query("id")
context.String(200, "ID is %s", id)
})
- 在 form 表单提交的时候,使用结构体去承接入参
在 web 包中的逻辑处理代码如下:
go
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
几个校验示例:
go
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 的基本流程如下:
- 使用 Protobuf 的语法定义消息类型和结构化数据的字段。
- 使用 Protobuf 编译器将定义的 Protobuf 文件(通常以 .proto 为后缀)转换为目标语言的代码文件。
- 在程序中使用生成的代码来序列化和反序列化消息。
总结起来,Protobuf 提供了一种跨语言、跨平台的数据序列化方案,使得不同系统之间可以高效、可靠地进行数据交换和通信。
7. 集成mysql数据库
1. gorm使用
gorm 是 golang 对于 sql 和对象的一种关系映射的框架,orm因为牵扯到映射,所以不推荐用于多表连接的操作。
使用go get -u gorm.io/gorm
下载依赖,以及go get -u gorm.io/driver/mysql
下mysql驱动。文档地址
- 创建连接池
然后在 middleware 文件夹下面新建 grom.go 文件
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) // 设置最大存活周期为半小时,超过这个周期还存在链接将会被回收
}
- 新增model
在 web 包下新建 models 包,新增 user.go 文件
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"
}
- 增删改查
web 下新增一个 dao 包,然后建立一个 user.go 文件
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: "",
},
}
增:
go
// 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)
}
删:其实现在大部分数据采用的是逻辑删除,删这个操作并不是特别重要,所以一个接口演示出所有的情况
go
// 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 更新表数据
go
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)
}
查:
go
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'
}