gin框架学习
- 1. 使用脚手架搭建gin框架
- 2. 应用框架
- 3. 路由管理
- 4.自定义中间件的使用
- 5. 通过中间件设置路由权限校验
- 1. 自定义校验
- 2. 配置跨域
- 3. 使用jwt进行tokn校验
- 6. 接口入参获取和绑定
- 2. 参数校验
- 3. protobuf
- 7. 集成mysql数据库
- 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 引擎。
package mainimport ("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 routerimport ("gin/web""github.com/gin-gonic/gin")func InitPublicApi(ginEngine *gin.Engine) {// 分组, ginEngine的Group是设置根目录 apiapi := ginEngine.Group("/api")// 设置次级目录 v1v1 := api.Group("/v1")// 设置 v1 下的子路径,假使这个路径是获取验证码,第二个参数就是具体的处理方式v1.GET("/verificationCode", web.VerificationCode)// 设置一个登录的 POST 路径v1.POST("/login", web.Login)}
同时 gin 目录下新建 web 文件夹,内部搭配新建 public.go 文件,然后用于处理路由指引过来的业务逻辑
package webimport ("fmt""github.com/gin-gonic/gin")func VerificationCode(context *gin.Context) {fmt.Println("生成了一个验证码")}func Login(context *gin.Context) {fmt.Println("处理了登录逻辑")}
在 router 下新建 routers.go 文件,在这里把所有的路由都创建出来
package routerimport "github.com/gin-gonic/gin"func InitRouters(ginEngine *gin.Engine) {InitPublicApi(ginEngine)InitUserApi(ginEngine)}
然后在 main 函数中调用即可
package mainimport ("github.com/gin-gonic/gin")func main() {// 获取 gin 引擎ginEngine := gin.Default()// 创建所有的路由router.InitRouters(ginEngine)// 运行 gin 进程, 默认 8080 端口,但是可以修改为任意的空闲端口,以下改变为了8081ginEngine.Run(":8081")}
4.自定义中间件的使用
在gin框架中,我们所有要对公共的处理都可以使用中间件来实现,所谓的中间件就是通过函数作为参数在完成本函数之前去额外做一些操作,由于。
首先现在gin目录下新建middleware文件夹,然后新建 myHandler.go 文件。
package middlewareimport ("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.useginEngine.Use(middleware.MyHandler())// 创建整个路由router.InitRouters(ginEngine)// 运行 gin 进程, 默认 8080 端口, 可以改变为任意空闲端口ginEngine.Run(":8081")}
当我们再次请求后台时,路径和方式就被打印出来了
5. 通过中间件设置路由权限校验
1. 自定义校验
在 middleware 文件夹下新建 token.go 文件,用来作为校验 token 的中间件
package middlewareimport ("errors""github.com/gin-gonic/gin""net/http")var token = "123456"func TokenCheck(context *gin.Context) {// 从请求头中获取tokenaccessToken := 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 routerimport ("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 routerimport ("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 middlewareimport "github.com/golang-jwt/jwt/v5"// 秘钥var key = "abcdefg123456"type Data struct {Id stringName stringAge intSex intjwt.RegisteredClaims // 这里就是使用鸭子模式,自动隐式判断继承}// Sign 签发tokenfunc Sign(data jwt.Claims) (string, error) {// 选择加密方式签发tokentoken := 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 middlewareimport ("errors""github.com/gin-gonic/gin""net/http")func TokenCheck(context *gin.Context) {// 从请求头中获取tokenaccessToken := 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. 接口入参获取和绑定
-
入参获取
-
在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)})
- 在传统的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)})
- 在 form 表单提交的时候,使用结构体去承接入参
在 web 包中的逻辑处理代码如下:
type loginReq struct {name string `form:"nickName" json:"nickName"` // 反括号内中 form是表名提交方式, nickName是表名提交的数据的keypassword 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 的基本流程如下:
- 使用 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 文件
package middlewareimport ("gorm.io/driver/mysql""gorm.io/gorm""gorm.io/gorm/logger""log""time")var DB *gorm.DBvar dsn = "admin:pass12345sx@tcp(127.0.0.1:3306)/hit-plum?charset=utf8mb4&parseTime=True&loc=Local"func init() {var err errorDB, err = gorm.Open(mysql.New(mysql.Config{DSN: dsn, // data source nameDefaultStringSize: 256, // 默认字符配置DisableDatetimePrecision: true, // 禁用日期时间精度,MySQL 5.6之前不支持此功能DontSupportRenameIndex: true, // 重命名索引时删除并创建,MySQL 5.7之前不支持重命名索引,MariaDBDontSupportRenameColumn: 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 文件
package modelsimport ("time""gorm.io/gorm")// 共有的字段type Model struct {gorm.Model // gorm 提供的共有字段UpdatedBy stringCreatedBy stringDeletedBy string}type User struct {Model Model `gorm:"embedded"` // 嵌入共有的一些字段UserName string `gorm:"column:user_name"` // 声明对应的列名Password stringRealName stringUserType uintEmail stringPhone stringDepartId intGender uintAvatar string `gorm:"type:text"` // 声明对应的字段数据库数据类型Enabled uintDelFlag uintLoginIp stringLoginDate time.TimeRemark string `gorm:"type:text"`}// TableName 方法指定表名, 默认是根据 结构体名字后加个s为表名,不符合此规范就要单独声明表名func (User) TableName() string {return "user"}
- 增删改查
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 := userTempDatares := middleware.DB.Create(&t)fmt.Println(res.RowsAffected)fmt.Println(res.Error)fmt.Println(t)}// PartialCreateUser 部分插入func PartialCreateUser() {t := userTempDatares := middleware.DB.Select("UserName", "Password", "CreatedBy").Create(&t)fmt.Println(res.RowsAffected)fmt.Println(res.Error)fmt.Println(t)}// OmitCreateUser 忽略某些字段插入func OmitCreateUser() {t := userTempDatares := 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 := userTempDatausers = 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: 8UserName: "lee",Password: "153654"}// SaveUser 修改一条数据func SaveUser() {t := tempmiddleware.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 = xmiddleware.DB.Model(&userTempData).Update("UserName", "hello")// 使用已有主键的model进行操作加where, 即 where id = x and enabled = 1middleware.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.Uservar users []models.Usermiddleware.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'}