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。

官方文档

go 复制代码
// 下载gin框架
go get -u github.com/gin-gonic/gin

2. gin关键技术点

2.1 数据解析和参数绑定

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

go 复制代码
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")
}

2.2 gin中间件

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

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

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

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

2.2.1 定义中间件

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

go 复制代码
// 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")
	}
}

PS:内置中间件

2.2.2 全局使用中间件

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

go 复制代码
// 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()
}

2.2.3 分组使用中间件

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

go 复制代码
// 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) {
	      ...
	   })
	}
}

2.2.4 单个路由使用中间件

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

go 复制代码
// 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()
}

2.3 gin路由组

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

go 复制代码
// 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()
}

3. gorm介绍

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

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

typescript 复制代码
## 必须安装gorm
go get -u gorm.io/gorm     
## 安装相应的数据库驱动。GORM 官方支持的数据库类型有: MySQL, PostgreSQL, SQlite, SQL Server
go get -u gorm.io/driver/mysql  

4. gorm常用示例

4.1 连接数据库

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

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

连接MySQL

go 复制代码
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()
}

连接PostgreSQL...等,详见

GORM

4.2 快速入门

go 复制代码
// 模型定义
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;
}

推荐:

普通场景:简单查询用Find+Where的函数结合实现,结合Limit+Offset+Order实现分页等高频功能;

追求性能:可以引入Select避免查询所有字段,但会导致返回结果部分字段不存在的奇怪现象,需要权衡;

复杂查询:例如Join+子查询等,推荐使用下面的原生SQL,用GORM拼接的体验并不好。

4.3 SQL是怎样生成的

两个核心文件

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

go 复制代码
db.Where(&User{Name: "小王子"}, "name", "Age").Find(&users)

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

核心-构建SQL的实现

go 复制代码
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...)
 }
}
相关推荐
m0_748232396 分钟前
WebRTC学习二:WebRTC音视频数据采集
学习·音视频·webrtc
虾球xz1 小时前
游戏引擎学习第55天
学习·游戏引擎
oneouto2 小时前
selenium学习笔记(二)
笔记·学习·selenium
sealaugh322 小时前
aws(学习笔记第十九课) 使用ECS和Fargate进行容器开发
笔记·学习·aws
炭烤玛卡巴卡2 小时前
学习postman工具使用
学习·测试工具·postman
thesky1234563 小时前
活着就好20241224
学习·算法
蜗牛hb3 小时前
VMware Workstation虚拟机网络模式
开发语言·学习·php
汤姆和杰瑞在瑞士吃糯米粑粑3 小时前
【C++学习篇】AVL树
开发语言·c++·学习
桃园码工3 小时前
5-Gin 静态文件服务 --[Gin 框架入门精讲与实战案例]
gin·实战案例·静态文件服务·入门精讲
虾球xz3 小时前
游戏引擎学习第58天
学习·游戏引擎