• gin & gorm学习笔记


    代码仓库
    https://gitee.com/zhupeng911/go-advanced.git
    https://gitee.com/zhupeng911/go-project.git

    1. gin介绍

    Gin 是使用纯 Golang 语言实现的 HTTP Web框架,Gin接口设计简洁,提供类似Martini的API,性能极高,现在被广泛使用。

    主要特性

    • 快速 - 基于 Radix 树的路由,小内存占用,没有反射,可预测的 API 性能。

    • 支持中间件 - 传入的 HTTP 请求可以由一系列**中间件**和最终操作来处理。 例如:Logger,Authorization,最终操作 DB。

    • 路由组 - 更好地组织路由。例如将需要授权和不需要授权的API分组,不同版本的api分组。分组可嵌套,且性能不受影响。

    • Crash 处理 - Gin 可以 catch 一个发生在 HTTP 请求中的 panic 并 recover 它。这样,你的服务器将始终可用。例如,你可以向 Sentry 报告这个 panic!

    • **JSON 验证 **- Gin 可以解析并验证请求的 JSON,例如检查所需值的存在。

    • 内置渲染 - Gin 原生为 JSON,XML 和 HTML 渲染提供了易于使用的 API。

    官方文档

    // 下载gin框架
    go get -u github.com/gin-gonic/gin
    
    • 1
    • 2

    2. gin关键技术点

    2.1 数据解析和参数绑定

    为了能够更方便的获取请求相关参数,提高开发效率,基于请求的Content-Type识别请求数据类型并利用反射机制自动提取请求中QueryString、form表单、JSON等参数到结构体中,后端接收并解析该结构体。

    type Student struct {
    	UserName     string `form:"user_name" json:"user_name" binding:"required"`
    	UserPassword string `form:"user_password" json:"user_password" binding:"required"`
    }
    
    func TestGin006() {
    	router := gin.Default()
    	// 1.绑定JSON的示例 ({"user_name": "小王子", "user_password": "123456"})
    	router.POST("/loginjson", func(c *gin.Context) {
    		var stu Student
    		// ShouldBind()会根据请求的Content-Type自行选择绑定器
    		if err := c.ShouldBind(&stu); err == nil {
    			fmt.Printf("stu: %v \n", stu)
    			c.JSON(http.StatusOK, gin.H{
    				"user_name":     stu.UserName,
    				"user_password": stu.UserPassword,
    			})
    		} else {
    			c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
    		}
    	})
    
    	// 2.绑定form表单示例 (user=小王子i&password=123456)
    	router.POST("/loginForm", func(c *gin.Context) {
    		var stu Student
    		if err := c.ShouldBind(&stu); err == nil {
    			c.JSON(http.StatusOK, gin.H{
    				"user":     stu.UserName,
    				"password": stu.UserPassword,
    			})
    		} else {
    			c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
    		}
    	})
    
    	// 3.绑定QueryString示例 (/loginQuery?user_name=q1mi&user_password=123456)
    	router.GET("/loginQuery", func(c *gin.Context) {
    		var stu Student
    		if err := c.ShouldBind(&stu); err == nil {
    			c.JSON(http.StatusOK, gin.H{
    				"user":     stu.UserName,
    				"password": stu.UserPassword,
    			})
    		} else {
    			c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
    		}
    	})
    
    	router.Run(":8080")
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50

    2.2 gin中间件

    中间件是Gin的精髓,一个个中间件组成一条中间件链,对HTTP Request请求进行拦截处理,实现了代码的解耦和分离,每个中间件只需要处理自己需要处理的业务。

    简单来说,Gin中间件的作用有两个:

    (1)Web请求到到达我们定义的HTTP请求处理方法之前,拦截请求并进行相应处理(比如:权限验证,数据过滤等),这个可以类比为前置拦截器前置过滤器;

    (2)在我们处理完成请求并响应客户端时,拦截响应并进行相应的处理(比如:添加统一响应部头或数据格式等),这可以类型为后置拦截器后置过滤器

    2.2.1 定义中间件

    在Gin框架中,中间件的类型定义如下所示,可以看出,中间件实际上就是一个以gin.Context为返回值的函数而已,与我们定义处理HTTP请求的Handler本质上是一样的,并没有什么神秘可言。

    // ProgramTimeCost a、定义中间件:统计接口耗时的中间件
    func ProgramTimeCost() gin.HandlerFunc {
    	return func(c *gin.Context) {
    		fmt.Printf("中间件---ProgramTimeCost in... \n")
    		start := time.Now()
    		c.Next() // 调用该请求的剩余处理程序
    		// c.Abort() // 不调用该请求的剩余处理程序
    		cost := time.Since(start)
    		fmt.Printf("该接口耗时: %v \n", cost)
    		fmt.Printf("中间件---ProgramTimeCost out... \n")
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    PS:内置中间件
    在这里插入图片描述

    2.2.2 全局使用中间件

    使用gin.Engine结构体的Use()方法便可以在所有请求应用中间件。

    // TestGin010 
    func TestGin010() {
    	r := gin.Default()
    	// 注册一个全局中间件,可以一次性注册多个中间件
    	r.Use(ProgramTimeCost())
    	r.GET("/index", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"msg": "index"}) })
    	r.GET("/login", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"msg": "login"}) })
    	r.Run()
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    2.2.3 分组使用中间件

    更多的时候,我们会根据业务不同划分不同路由分组(RouterGroup ),不同的路由分组再应用不同的中间件,在这种下就是在路由分组中局部使用中间件。

    // TestGin010 
    func TestGin010() {
    	router := gin.New()
    	user := router.Group("user", ProgramTimeCost()
    	{
    	   user.GET("info", func(context *gin.Context) {
    	      ...
    	   })
    	   user.GET("article", func(context *gin.Context) {
    	      ...
    	   })
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    2.2.4 单个路由使用中间件

    除了路由分组,在单个请求路由中也可以应用中间件

    // TestGin010 
    func TestGin010() {
    	r := gin.Default()
    	// 给路由单独注册中间件(一个接口可注册多个中间件)
    	r.GET("/logout", ProgramTimeCost(), func(c *gin.Context) {
    		c.JSON(http.StatusOK, gin.H{
    			"msg":  "logout",
    			"name": name,
    		})
    	})
    	r.Run()
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    2.3 gin路由组

    gin 框架中采用的路由库是基于httprouter实现的,并支持Restful风格的API。为了管理具有相同前缀的URL, 将拥有URL共同前缀的路由划分为一组。

    // TestGin009 路由组
    func TestGin009() {
    	r := gin.Default()
    	// 1.普通路由
    	r.GET("/index", func(c *gin.Context) {})
    	r.GET("/login", func(c *gin.Context) {})
    	r.POST("/login", func(c *gin.Context) {})
    
    	// 2.没有匹配到路由的请求返回统一404页面
    	r.LoadHTMLFiles("./static/404.html")
    	r.NoRoute(func(c *gin.Context) {
    		c.HTML(http.StatusNotFound, "404.html", nil)
    	})
    
    	//3.路由组 将拥有共同URL前缀的路由划分为一个路由组
    	//为路由组注册中间件方法一
    	userGroup := r.Group("/user", ProgramTimeCost())
    	{
    		userGroup.GET("/queryName", func(c *gin.Context) {
    			c.JSON(http.StatusOK, gin.H{
    				"path": "/user/queryName",
    			})
    		})
    		userGroup.GET("/queryAge", func(c *gin.Context) {
    			c.JSON(http.StatusOK, gin.H{
    				"path": "/user/queryAge",
    			})
    		})
    
    		// 路由组也是支持嵌套的
    		//为路由组注册中间件方法二
    		shopGroup := userGroup.Group("/shop")
    		shopGroup.Use(ProgramTimeCost())
    		{
    			shopGroup.GET("/queryShopName", func(c *gin.Context) {
    				c.JSON(http.StatusOK, gin.H{
    					"path": "/user/shop/queryShopName",
    				})
    			})
    			shopGroup.GET("/queryShopAddr", func(c *gin.Context) {
    				c.JSON(http.StatusOK, gin.H{
    					"path": "/user/shop/queryShopAddr",
    				})
    			})
    		}
    	}
    	r.Run()
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48

    3. gorm介绍

    The fantastic ORM library for Golang aims to be developer friendly.

    gorm是GoLang实现的,在GitHub上活跃度很高的对象关系映射框架(ORM)。它的作用是在关系型数据库和对象之间作一个映射,这样,我们在具体的操作数据库的时候,就不需要再去和复杂的SQL语句打交道,只要像平时操作对象一样操作它就可以了。

    ## 必须安装gorm
    go get -u gorm.io/gorm     
    ## 安装相应的数据库驱动。GORM 官方支持的数据库类型有: MySQL, PostgreSQL, SQlite, SQL Server
    go get -u gorm.io/driver/mysql  
    
    • 1
    • 2
    • 3
    • 4

    4. gorm常用示例

    4.1 连接数据库

    连接不同的数据库都需要导入对应数据的驱动程序,GORM已经贴心的为我们包装了一些驱动程序,只需要按如下方式导入需要的数据库驱动即可:

    GORM 官方支持的数据库类型有: MySQL, PostgreSQL, SQlite, SQL Server

    连接MySQL

    import (
      "github.com/jinzhu/gorm"
      _ "github.com/jinzhu/gorm/dialects/mysql"
    )
    
    func main() {
      db, err := gorm.Open("mysql", "username:password@(localhost)/dbname?charset=utf8mb4&parseTime=True&loc=Local")
      defer db.Close()
    }
    
    // MySQl 驱动程序提供了 一些高级配置 可以在初始化过程中使用,例如:
    func main() {
    	db, err := gorm.Open(mysql.New(mysql.Config{
    	  DSN: "gorm:gorm@tcp(127.0.0.1:3306)/gorm?charset=utf8&parseTime=True&loc=Local", // DSN data source name
    	  DefaultStringSize: 256, // string 类型字段的默认长度
    	  DisableDatetimePrecision: true, // 禁用 datetime 精度,MySQL 5.6 之前的数据库不支持
    	  DontSupportRenameIndex: true, // 重命名索引时采用删除并新建的方式,MySQL 5.7 之前的数据库和 MariaDB 不支持重命名索引
    	  DontSupportRenameColumn: true, // 用 `change` 重命名列,MySQL 8 之前的数据库和 MariaDB 不支持重命名列
    	  SkipInitializeWithVersion: false, // 根据当前 MySQL 版本自动配置
    	}), &gorm.Config{})
      defer db.Close()
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    连接PostgreSQL…等,详见

    GORM

    4.2 快速入门

    // 模型定义
    type DFUser struct {
    	gorm.Model              // gorm.Model是一个包含了ID, CreatedAt, UpdatedAt, DeletedAt四个字段的Golang结构体
    	UserId           uint64 `gorm:"column:user_id;type:int;primary_key;unique;not null"`
    	UserName         string `gorm:"column:user_name;type:varchar(255)"`
    	UserAge          int    `gorm:"column:user_age;default:0"`
    	UserMemberNumber string `gorm:"unique_index;not null"` // 设置会员号(member number)唯一并且不为空
    	UserAddress      string `gorm:"index:addr"`            // 给UserAddress字段创建名为addr的索引
    	UserBirthday     time.Time
    	IgnoreMe         int `gorm:"-"` // 忽略本字段
    }
    
    // 设置表名001
    func (DFUser) TableName() string {
    	return "df_user"
    }
    
    func TestGorm001() {
    	// 连接数据库mysql
    	db, err := gorm.Open("mysql", "root:zhupeng123@(127.0.0.1:3306)/go_db_1?charset=utf8&parseTime=True&loc=Local")
    	if err != nil {
    		panic(err)
    	}
    	defer db.Close()
    
    	// 操作数据库-建表
    	db.AutoMigrate(&DFUser{})					 // 自动迁移【结构体与表对应,类似于JPA】
    	//db.Table("df_user").CreateTable(&DFUser{}) // 手动设置表名002
    	//db.SingularTable(true)					 // 禁用默认表名的复数形式,如果置为 true,则 `User` 的默认表名是 `user`
    
    	// 表中添加数据
    	dfUser01 := DFUser{UserId: 1000, UserName: "小王子", UserAge: 23, UserMemberNumber: "VIP1000", UserAddress: "江苏南京", UserBirthday: time.Now()}
    	dfUser02 := DFUser{UserId: 1001, UserName: "小王子2", UserAge: 24, UserMemberNumber: "VIP1001", UserAddress: "江苏南京", UserBirthday: time.Now()}
    	db.Create(&dfUser01)
    	db.Create(&dfUser02)
    
    	// 查询
    	var user DFUser
    	db.First(&user)    // 根据主键查询第一条记录 SELECT * FROM df_user ORDER BY id LIMIT 1;
    	db.Last(&user)     // 根据主键查询最后一条记录 SELECT * FROM df_user ORDER BY id DESC LIMIT 1;
    	db.Find(&user)     // 查询所有的记录 SELECT * FROM df_user;
    	
    	db.Where(&DFUser{UserName: "小王子", UserAge: 23}).First(&user)
    	// SELECT * FROM df_user WHERE user_name = "朱鹏" AND user_age = 23 LIMIT 1;
    
    	// 更新某个字段
    	db.Model(&user).Update("user_name", "zhupeng123")
    	// UPDATE users SET name='zhupeng123' WHERE id=111;
    
    	// 更新多个字段
    	db.Model(&user).Updates(DFUser{UserName: "zhupeng_update", UserAge: 0, UserBirthday: time.Now()})
    	// UPDATE users SET user_name='zhupeng_update', user_age=18, updated_at='2013-11-17 21:34:10' WHERE id=111;
    
    	// 批量更新
    	db.Where("id IN (?)", []int{10, 11}).Updates(map[string]interface{}{"user_name": "hello", "user_age": 18})
    	// UPDATE users SET user_name='hello', user_age=18 WHERE id IN (10, 11);
    
    	// 删除记录
    	db.Delete(&user)
    	// DELETE from df_user where id=10;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61

    推荐:
    普通场景:简单查询用Find+Where的函数结合实现,结合Limit+Offset+Order实现分页等高频功能;
    追求性能:可以引入Select避免查询所有字段,但会导致返回结果部分字段不存在的奇怪现象,需要权衡;
    复杂查询:例如Join+子查询等,推荐使用下面的原生SQL,用GORM拼接的体验并不好。

    4.3 SQL是怎样生成的

    两个核心文件

    在GORM库中,有两个核心的文件,也是我们调用频率最高的函数所在:chainable_api.go和 finisher_api.go。顾名思义,前者是整个链式调用的中间部分,后者则是最终获取结果的函数。以查询为例:

    db.Where(&User{Name: "小王子"}, "name", "Age").Find(&users)
    
    • 1

    其中Where是chainable,也就是还在拼接SQL条件,Find则是触发真正查询的finisher,从finisher入手,看看一个SQL的到底是怎么在GORM中拼接并执行的。

    核心-构建SQL的实现

    func BuildQuerySQL(db *gorm.DB) {
      // SQL为空,表示需要自己构建
     if db.Statement.SQL.String() == "" {
      db.Statement.SQL.Grow(100) // 分配初始空间
    
      if len(db.Statement.Selects) > 0 {
          // 表示只select某几个字段,而不是select *
      } else if db.Statement.Schema != nil && len(db.Statement.Omits) > 0 {
          // Omit表示忽略特定字段
      } else if db.Statement.Schema != nil && db.Statement.ReflectValue.IsValid() {
          // 查询到指定结构体
      }
    
      // 对join的处理,涉及到多表关联,暂时忽略
      if len(db.Statement.Joins) != 0 {
      } else {
       db.Statement.AddClauseIfNotExists(clause.From{})
      }
    
        // 用一个map去重,符合名字中的 IfNotExists 含义
      db.Statement.AddClauseIfNotExists(clauseSelect)
    
        // 最后拼接出完整 SQL 的地方
      db.Statement.Build(db.Statement.BuildClauses...)
     }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
  • 相关阅读:
    OpenCV 如何实现边缘检测器
    哈希
    使用 Python 创建您自己的NFT集合(二)NFT存储
    Oracle GoldenGate(OGG)到入土
    每日一题——LeetCode1496.判断路径是否相交
    asp.net班级管理系统VS开发sqlserver数据库web结构c#编程Microsoft Visual Studio
    TiDB 集群故障诊断
    什么是自动化测试,一文吃透自动化测试【实战总结/建议收藏】
    Java的集合
    ceisum初始化
  • 原文地址:https://blog.csdn.net/qq_21880261/article/details/136453696