前言
Gin是一个用Go语言编写的web框架。它是一个类似于martini但拥有更好性能的API框架, 由于使用了httprouter,速度提高了近40倍。
如果你是性能和高效的追求者, 你会爱上Gin。
Go语言里最流行的Web框架,Github上有32K+star。 基于httprouter开发的Web框架。 中文文档齐全,简单易用的轻量级框架。
安装
D:learning-gin>set GOPROXY=https://goproxy.cn
-------------------------------------------------------
D:learning-gin>go get -u github.com/gin-gonic/gin
go: google.golang.org/protobuf upgrade => v1.25.0 go: gopkg.in/yaml.v2 upgrade => v2.4.0 go: github.com/golang/protobuf upgrade => v1.4.3 go: github.com/ugorji/go/codec upgrade => v1.2.1 go: golang.org/x/sys upgrade => v0.0.0-20201211090839-8ad439b19e0f go: github.com/json-iterator/go upgrade => v1.1.10 go: github.com/modern-go/reflect2 upgrade => v1.0.1 go: github.com/go-playground/validator/v10 upgrade => v10.4.1 go: github.com/modern-go/concurrent upgrade => v0.0.0-20180306012644-bacd9c7ef1dd go: golang.org/x/crypto upgrade => v0.0.0-20201208171446-5f87f3452ae9
Gin简单示例
package main import "github.com/gin-gonic/gin" func index(c *gin.Context) { //返回json类型的数据,h=type H map[string]interface{} c.JSON(200, gin.H{"msg": "您好呀!"}, ) } func main() { //定义1个默认路由(基于httprouter的) router := gin.Default() //增加url router.GET("/index", index) //server段开始linsten运行 router.Run("127.0.0.1:8000") }
Gin request
我们可以通过gin的context获取到客户端请求携带的url参数、form表单、json数据、文件等。
package main import ( "fmt" "github.com/gin-gonic/gin" ) type user struct { Name string `json:"name"` City string `json:"city"` } var person=&user{} //从url获取参数 func urlData(c *gin.Context) { //name:=c.Query("name") //获取url参数,获取不到获取空字符串 name:=c.DefaultQuery("name","zhanggen") ////获取url参数,获取不到获取默认! city:=c.DefaultQuery("city","bj") person.Name=name person.City=city c.JSON(200,person) } //从form表单中获取数据 func formData(c *gin.Context) { c.PostForm("name") person.Name=c.DefaultPostForm("name","Martin") person.City=c.DefaultPostForm("city","London") c.JSON(200,person) } //获取url地址参数 func pathData(c *gin.Context){ person.City=c.Param("city") person.Name=c.Param("name") c.JSON(200,*person) } //获取json数据 func jsonData(c *gin.Context){ c.Bind(person) fmt.Println("-----------------",*person) c.JSON(200,person) } func main() { r:=gin.Default() //http://127.0.0.1:8001/user?name=zhanggen&city=beijing r.GET("/user",urlData) r.POST("/user",formData) //http://127.0.0.1:8001/user/bj/zhanggen r.GET("/user/:city/:name",pathData) r.POST("/user/json/",jsonData) r.Run(":8001") }
Gin shouldBind
默认情况下,我们需要根据客户端请求的content-type,在后端使用不同的方式,获取客户端请求参数。
获取个请求参还需要c.Query、c.PostForm、c.Bind、C.Param,这也太麻烦了~
shouldBind可帮助我们根据客户端request的content-type,自动获取请求参数,并赋值给后端struct的字段。
package main
import (
"fmt"
"github.com/gin-gonic/gin"
)
//0.contentType对应ShouldBind对应的结构体
type UserInfo struct {
Username string `form:"username" json:"username"`
Password string `form:"password" json:"password"`
}
func index(c *gin.Context) {
requestMethod := c.Request.Method
//1.声明1个值类型uerinfo类型的变量u
var user UserInfo
//2.把客户端request请求的参数和后端的结合体字段进行绑定
err := c.ShouldBind(&user)
if err != nil {
c.JSON(400, gin.H{"err": err.Error()})
return
}
//3.可以通过反射的方式,根据客户端request的contentType自动获取数据了
if requestMethod == "GET" {
fmt.Println(user)
c.HTML(200, "index.html", gin.H{})
}
if requestMethod == "POST" {
fmt.Println(user)
c.JSON(200, gin.H{"data": "postOkay"})
}
}
func main() {
router := gin.Default()
router.Static("/static", "./static")
router.LoadHTMLGlob("templates/*")
router.GET("/user", index)
router.POST("/user", index)
router.Run(":8002")
}
Gin response
我们web开发过程中,大型项目会采用MVVM(前后端分离)的架构,小型项目会采用MTV(模板渲染)的架构。
疏通同归其目的都是完成数据驱动视图,不同的是数据驱动视图的地方不一样。
貌似web开发玩得就是这6个字,数据----》 驱动-----》视图。空谈误国,怎么才能更好的驱动视图才是关键。
MTV模式(模板渲染):后端使用模板语法也就是字符串替换的方式,在后端直接完成数据和HTML的渲染,直接返回给客户端。
MVVM(前后端分离架构):后端返回json数据,前端使用axios/ajax的方式获取到数据,使用vue等前端框架完成数据到HTML的渲染。
1.RESTful API
只要API程序遵循了REST风格,那就可以称其为RESTful API。
其实核心思想是1个资源对应1个URL,客户端对这1资源操作时(不同的request.get/post/put/delete方法)对应后端的增/删/改/查操作。
例如,我们现在要编写一个管理书籍的系统,我们可以查询对一本书进行查询、创建、更新和删除等操作。
我们在编写程序的时候就要设计客户端浏览器与我们Web服务端交互的方式和路径。按照经验我们通常会设计成如下模式:
| 请求方法 | URL | 含义 |
|---|---|---|
| GET | /book | 查询书籍信息 |
| POST | /create_book | 创建书籍记录 |
| POST | /update_book | 更新书籍信息 |
| POST | /delete_book | 删除书籍信息 |
我们按照RESTful API设计如下:
| 请求方法 | URL | 含义 |
|---|---|---|
| GET | /book | 查询书籍信息 |
| POST | /book | 创建书籍记录 |
| PUT | /book | 更新书籍信息 |
| DELETE | /book | 删除书籍信息 |
c.JSON响应json数据
package main import "github.com/gin-gonic/gin" //结构体 type user struct { Name string `json:"name"` Age int `json:"age"` } //视图函数 func perosn(c *gin.Context) { var userInfor=user{Name: "张根",Age:18} c.JSON(200,userInfor) } func main(){ r:=gin.Default() r.GET("/person/",perosn) r.Run(":8002") }
2.MVC模板渲染
如果是小型项目、历史原因、SEO优化我们使用模板渲染,Gin也是支持MTV模式的。
package main import ( "fmt" "github.com/gin-gonic/gin" ) func index(c *gin.Context) { //3.gin 模板渲染 c.HTML(200, "index.html", gin.H{"title": "首页", "body": "hello"}) } func main() { //1.创建1个默认的路由引擎 router := gin.Default() router.GET("/", index) //2.gin模板解析 router.LoadHTMLGlob("templates/*") //正则匹配templates/所有文件 router.LoadHTMLGlob("templates/**/*") //正则匹配template/目录/所有文件 err := router.Run(":9001") if err != nil { fmt.Println("gin启动失败", err) } }
3.文件上传
http请求也可以传输文件,有时候我们可以使用gin搭建1个ftp服务器。
单个文件上传
package main
import (
"fmt"
"github.com/gin-gonic/gin"
"path"
)
func handleFile(c *gin.Context) {
method := c.Request.Method
if method == "GET" {
c.HTML(200, "file.html", gin.H{})
}
if method == "POST" {
//1.从客户端请求中获取文件
fileObj, err := c.FormFile("localFile")
if err != nil {
c.JSON(400, gin.H{"err": err.Error()})
return
}
//2.保存到服务端
fileStorePath := path.Join("./upload/", fileObj.Filename)
err = c.SaveUploadedFile(fileObj, fileStorePath)
if err != nil {
errMsg := fmt.Sprintf("文件保存失败:%s
", err.Error())
c.JSON(200, gin.H{"err": errMsg})
}
c.JSON(200, gin.H{"data": "上传成功"})
}
}
func main() {
router := gin.Default()
router.Static("/static", "./static")
router.LoadHTMLGlob("templates/*")
router.GET("/file/", handleFile)
router.POST("/file/", handleFile)
err := router.Run(":8002")
if err != nil {
fmt.Println("gin启动失败", err)
return
}
}
多个文件上传
func main() {
router := gin.Default()
// 处理multipart forms提交文件时默认的内存限制是32 MiB
// 可以通过下面的方式修改
// router.MaxMultipartMemory = 8 << 20 // 8 MiB
router.POST("/upload", func(c *gin.Context) {
// Multipart form
form, _ := c.MultipartForm()
files := form.File["file"]
for index, file := range files {
log.Println(file.Filename)
dst := fmt.Sprintf("C:/tmp/%s_%d", file.Filename, index)
// 上传文件到指定的目录
c.SaveUploadedFile(file, dst)
}
c.JSON(http.StatusOK, gin.H{
"message": fmt.Sprintf("%d files uploaded!", len(files)),
})
})
router.Run()
}
Gin模板渲染
现在大部分都是前后端分离的架构,除了seo优化我们基本不会使用gin做模板渲染。
1.扩展gin模板函数
package main import ( "fmt" "github.com/gin-gonic/gin" "html/template" ) func index(c *gin.Context) { //3.gin 模板渲染 c.HTML(200, "index.html", gin.H{"title": "首页", "name": "Martin", "age": "hello", "url": `<a href="https://www.cnblogs.com/sss4/">主页</a>`}) } func main() { //1.创建1个默认的路由引擎 router := gin.Default() router.GET("/", index) //1.5 gin框架模板自定义模板函数 router.SetFuncMap(template.FuncMap{ "safe": func(safeString string) template.HTML { return template.HTML(safeString) }, }) //2.gin模板解析 //router.LoadHTMLGlob("templates/*") //正则匹配templates/所有文件 router.LoadHTMLGlob("templates/**/*") //正则匹配template/目录/所有文件 err := router.Run(":9001") if err != nil { fmt.Println("gin启动失败", err) } }
模板
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>{{.title}}</title> </head> <body> <ul> <li>{{.name}}</li> <li>{{.age}}</li> <li>{{.url | safe}}</li> </ul> </body> </html>
2.加载静态文件路径
package main
import (
"fmt"
"github.com/gin-gonic/gin"
"html/template"
)
func index(c *gin.Context) {
//5.gin 模板渲染
c.HTML(200, "index.html", gin.H{"title": "首页", "name": "Martin", "age": "hello", "url": `<a href="https://www.cnblogs.com/sss4/">主页</a>`})
}
func main() {
//1.创建1个默认的路由引擎
router := gin.Default()
router.GET("/", index)
//2.加载静态文件路径 .css
router.Static("/static","./static")
//3. 扩展gin框架模板自定义模板函数
router.SetFuncMap(template.FuncMap{
"safe": func(safeString string) template.HTML {
return template.HTML(safeString)
},
})
//4.gin模板解析
//router.LoadHTMLGlob("templates/*") //正则匹配templates/所有文件
router.LoadHTMLGlob("templates/**/*") //正则匹配template/目录/所有文件
err := router.Run(":9001")
if err != nil {
fmt.Println("gin启动失败", err)
}
}
模板
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>{{.title}}</title> <link rel="stylesheet" href="/static/dist/css/bootstrap.min.css"> <script src="/static/jquery-3.2.1.min.js"></script> <script src="/static/dist/js/bootstrap.min.js"></script> </head> <body> <div class="container"> <table class="table table-hover"> <theader> <tr> <td>姓名</td> <td>年龄</td> <td>主页</td> </tr> </theader> <tbody> <tr> <td>{{.name}}</td> <td>{{.age}}</td> <td>{{.url|safe}}</td> </tr> </tbody> </table> </div> </body> </html>
3.Gin模板继承
html/template实现了模板的嵌套和继承,但是gin不包含此功能。但是我们使用第第三方包。github.com/gin-contrib/multitemplate
Gin 路由组
URL路由太多了就需要分组管理,类似Flask的蓝图、Django里面的include URL。这些都是基于反射实现的。
但是Gin框架中的路由使用的是httprouter这个库,其基本原理就是构造一个路由地址的前缀树。
1.单支路由
//所有请求方式都汇聚到handleBook router.Any("/book/", handleBook) //处理404错误 router.NoRoute(handle404)
2.路由组
//cmdb路由组 cmdbRouter := router.Group("/cmdb") { cmdbRouter.GET("/list/") cmdbRouter.GET("/hosts/") cmdbRouter.GET("/option/") } //工单路由组 workOrder := router.Group("/works") { workOrder.GET("/daily/") cmdbRouter.GET("/momthly") cmdbRouter.GET("/quarterly") }
3.路由嵌套
虽然gin的路由支持嵌套,但是出于对查询性能的考虑我们一般都会不会嵌套很多层路由。
//cmdb路由组 cmdbRouter := router.Group("/cmdb") { cmdbRouter.GET("/list/") cmdbRouter.GET("/hosts/") //1.cmdb的主机 hostRouter := cmdbRouter.Group("/host") { //1.1主机的cpu hostRouter.GET("/cup/") //1.2主机的内存 hostRouter.GET("/memory/") //1.3主机的硬盘 hostRouter.GET("/disks/") //1.4主机运行的服务 hostRouter.GET("/process/") //1.5网络流量 hostRouter.GET("/networks/") } cmdbRouter.GET("/option/") } //2.工单路由组 workOrder := router.Group("/works") { workOrder.GET("/daily/") cmdbRouter.GET("/momthly") cmdbRouter.GET("/quarterly") }
Gin中间件
我们可以在不修改视图函数的前提下,利用Web框架中携带的钩子函数也就是中间件 做权限控制、登录认证、权限校验、数据分页、记录日志、耗时统计.........
注意我们的中间件不仅可以设置1个,也根据我们的业务逻辑设置N个,相当于对用户请求增加了多层过滤。
就像Python里面的多层装饰器。
1.中间件执行流程

由于http请求包含request、response 2个动作所以中间件是双行线,中间件的执行流程就像1个递归函数的执行过程。
压栈: 用户---------> 认证中间件---------> 用户权限中间件---------> 错误处理中间件---------> 视图函数执行 出栈: 视图函数执行完毕---------> 错误处理中间件---------> 用户权限中间件---------> 认证中间件---------> 用户
2.控制中间件执行流程
所为的控制流程我感觉就是设计中间件这个栈里面包含的层层栈针。
我们在弹匣里装了什么样的子弹,扣动扳机时就会发射出什么子弹,这样想会更简单一些否则很容易被绕进去。
在中间件执行的过程中我们可以控制进栈和出栈流程。

以上代码执行结果:
m1 in
m2 in
m1 out
调用context.Next(),继续调用下一个视图函数进行压栈。(子弹装满弹匣)
调用context.Abort() 阻止继续调用后续的函数,执行完当前栈针(函数)之后出栈。(1发子弹就够了)
调用context.Abort() + return,当前位置返回,当前位置之后的代码都不需要不执行了。(1发哑弹)
package main
import (
"fmt"
"github.com/gin-gonic/gin"
)
//中间件1
func middleWare1(c *gin.Context) {
fmt.Println("middleWare1开始----------")
c.Next() //调用后续的处理函数
fmt.Println("middleWare1结束----------")
}
//中间件2
func middleWare2(c *gin.Context) {
fmt.Println("middleWare2开始========")
c.Abort()//终止后续处理函数的调用,执行完本函数返回
return //更极端一些 到这就结束!(本函数也不需要执行完毕了)。
fmt.Println("middleWare2结束========")
}
//index视图函数
func index(c *gin.Context) {
fmt.Println("index开始+++++++++")
c.JSON(200, gin.H{"data": "ok"})
fmt.Println("index结束+++++++++")
}
func main() {
router := gin.Default()
//全局注册中间件:middleWare1, middleWare2
router.Use(middleWare1, middleWare2)
router.GET("/index/", index)
err := router.Run(":9001")
if err != nil {
fmt.Println("Gin启动失败", err)
}
}
3.给单个路由(url)设置中间件
当我们需要对特定的视图函数增加新功能时,可以给它增加1个中间件。
package main import ( "fmt" "github.com/gin-gonic/gin" "time" ) //中间件1 func middleWare1(c *gin.Context) { fmt.Println("--------------I`m going through middleWare1----------") start := time.Now() c.Next() //调用后续的处理函数 cost := time.Since(start) fmt.Printf("耗时----------%v ", cost) c.Abort() //终止请求 } //index handlerfunc类型的函数 func index(c *gin.Context) { fmt.Println("--------------I`m going through handlerfunc----------") c.JSON(200, gin.H{"data": "ok"}) c.Next() } //中间件2 func middleWare2(c *gin.Context) { fmt.Println("--------------I`m going through middleWare2----------") c.Next() c.Abort() //请求终止 } func main() { router := gin.Default() //设置中间件流程:middleWare1-----》index----》middleWare2 router.GET("/index/", middleWare1, index, middleWare2) err := router.Run(":9001") if err != nil { fmt.Println("Gin启动失败", err) } }
4.全局注册中间件
如果我们需要每个视图函数都设置1个中间件,把这一中间件写到每个视图函数前面会非常不方便,我们可以使用use进行全局注册。
package main import ( "fmt" "github.com/gin-gonic/gin" ) //中间件1 func middleWare1(c *gin.Context) { fmt.Println("middleWare1开始----------") c.Next() //调用后续的处理函数 fmt.Println("middleWare1结束----------") //c.Abort() //终止请求 } //中间件2 func middleWare2(c *gin.Context) { fmt.Println("middleWare2开始========") c.Next() fmt.Println("middleWare2结束========") } //index handlerfunc类型的函数 func index(c *gin.Context) { fmt.Println("index开始+++++++++") c.JSON(200, gin.H{"data": "ok"}) fmt.Println("index结束+++++++++") } func main() { router := gin.Default() //全局注册中间件:middleWare1, middleWare2 router.Use(middleWare1, middleWare2) router.GET("/index/", index) err := router.Run(":9001") if err != nil { fmt.Println("Gin启动失败", err) } }
输出:
验证web框架里中间件设计思想是的递归思想。
middleWare1开始---------- middleWare2开始======== index开始+++++++++ index结束+++++++++ middleWare2结束======== middleWare1结束----------
5.路由组注册中间件
给路由组注册中间件有2种写法
写法1:
shopGroup := r.Group("/shop", StatCost())
{
shopGroup.GET("/index", func(c *gin.Context) {...})
...
}
写法2:
shopGroup := r.Group("/shop")
shopGroup.Use(StatCost())
{
shopGroup.GET("/index", func(c *gin.Context) {...})
...
}
6.闭包的中间件
以上我们得知:Gin的中间件是以1种gin.HandlerFunc类型存在,在路由和路由组里进行注册。
router.GET("/index/", authMiddleWare(false), index)
那我们可以使用闭包将1个开关参数和这个handlerFunc一起包起来。实现对中间进行开关控制比较灵活。
package main
import (
"fmt"
"github.com/gin-gonic/gin"
)
//使用闭包函数返回,gin.HandlerFunc。可以实现对中间进行开关控制,比较灵活。
func authMiddleWare(work bool) gin.HandlerFunc {
if work{
//连接数据库
//其他准备工作
dbDB := "Martin"
return func(c *gin.Context) {
username := c.Query("username")
if username == dbDB {
c.Next()
} else {
c.Abort()
c.JSON(403, gin.H{"data": "没有访问权限"})
}
}
}
return func(context *gin.Context) {
}
}
//index视图函数
func index(c *gin.Context) {
fmt.Println("index视图函数开始")
c.JSON(200, gin.H{"data": "ok"})
fmt.Println("index视图函数结束")
}
func main() {
router := gin.Default()
router.GET("/index/", authMiddleWare(false), index)
err := router.Run(":9001")
if err != nil {
fmt.Println("Gin启动失败", err)
}
}
7.夸中间件进行传值
中间件可以有多层,假如我们上游的中间得出的值,如何传递到下游中间件呢?。通过上下文content。
当在中间件或handler中启动新的goroutine时,不能使用原始的上下文(c *gin.Context),必须使用其只读副本(c.Copy())。以保证我们传递的值是一致的。
c.Set("username", username)
currentUser, ok := c.Get("username")
8.gin默认中间件
gin.Defaut生成的路由引擎,默认使用了Logger(), Recovery()的中间件。
//生成的路由引擎,默认使用了Logger(), Recovery()的中间件 gin.Default()
router := gin.New()
Logger:用于记录日志
Recovery:用于保证在gin 发生错误时进程不会终止。
package main import ( "fmt" "github.com/gin-gonic/gin" ) //使用闭包函数返回,gin.HandlerFunc。可以实现对中间进行开关控制,比较灵活。 func authMiddleWare(work bool) gin.HandlerFunc { if work { //连接数据库 //其他准备工作 dbDB := "Martin" return func(c *gin.Context) { username := c.Query("username") if username == dbDB { //1.在中间中设置值进行传递 c.Set("username", username) fmt.Println("-----------", username) c.Next() } else { c.Abort() c.JSON(403, gin.H{"data": "没有访问权限"}) } } } return func(context *gin.Context) { } } //index视图函数 func index(c *gin.Context) { //2.获取上流传递的值 currentUser, ok := c.Get("username") if !ok { currentUser = "anonymous" } fmt.Println("index视图函数开始") c.JSON(200, gin.H{"data": currentUser}) fmt.Println("index视图函数结束") } func main() { //生成的路由引擎,默认使用了Logger(), Recovery()的中间件 gin.Default() //router := gin.Default() router := gin.New() router.GET("/index/", authMiddleWare(true), index) err := router.Run(":9001") if err != nil { fmt.Println("Gin启动失败", err) } }
Gin设置cookie
1.cookie是server端保存在browser中的用户信息
2.每客户端次访问server端时都会携带该域下的cooki信息到server端。
3.不同域名之间的cookie是不共享的。