Golang学习笔记(08-2-1-Gin框架快速入门)

258 阅读10分钟

1. Gin简单入门

Gin 框架 现在是 github 上 start 最多 Go 语言编写的 Web 框架,相比其他它的几个 start 数量差不多的框架,它更加的轻量,有更好的性能。它使用了httprouter,如果你是性能和高效的追求者, 你会爱上Gin!

1.1. 简单示例

1.1.1. 第一个gin程序

import (
	"github.com/gin-gonic/gin"
	log "github.com/sirupsen/logrus"
)

func main() {
	r := gin.Default()  // 创建默认路由引擎

	// 配置 location 为 / 时,且请求为GET时的路由处理函数
	r.GET("/", func(c *gin.Context) {
		// c.JSON() 构造json格式的响应体,在后端应用中非常常见
		// 请求和响应的响应信息都封装在了对象 c 中
		c.JSON(200, gin.H{
			"method" : c.Request.Method,
			"location": c.Request.URL.String(),
			"message": "Hello world!",
		})
	})

	err := r.Run("0.0.0.0:8080")  // 绑定 8080 端口
	if err != nil {
		log.Fatal("Init gin server failed,err:%s", err.Error())
	}
}
[root@duduniao ~]# curl 127.0.0.1:8080
{"location":"/","message":"Hello world!","method":"GET"}

1.1.2. RestFul API

在后端开发中,RestFul API 非常常见,即location就是目标资源,而不同的请求方法代表对这个请求资源的操作方式,如:

请求方法URL含义
GET/book/ISBN6379查询书籍信息
HEADA/book/ISBN6379查询书籍是否存在
POST/book/ISBN6379创建书籍记录
PUT/book/ISBN6379更新书籍信息
DELETE/book/ISBN6379删除书籍信息
import (
	"github.com/gin-gonic/gin"
	log "github.com/sirupsen/logrus"
)

func main()  {
	r := gin.Default()

	// GET
	r.GET("/book/ISBN6379", func(c *gin.Context) {
		c.JSON(200, gin.H{
			"method": c.Request.Method,
		})
	})

	// POST
	r.POST("/book/ISBN6379", func(c *gin.Context) {
		c.JSON(200, gin.H{
			"method": c.Request.Method,
		})
	})

	// PUT
	r.PUT("/book/ISBN6379", func(c *gin.Context) {
		c.JSON(200, gin.H{
			"method": c.Request.Method,
		})
	})

	// DELETE
	r.DELETE("/book/ISBN6379", func(c *gin.Context) {
		c.JSON(200, gin.H{
			"method": c.Request.Method,
		})
	})

	if err := r.Run("0.0.0.0:8080"); err != nil {
		log.Fatal("Init web failed, err:%s\n", err.Error())
	}
}
[root@duduniao ~]# for i in GET POST PUT DELETE;do curl -X $i http://127.0.0.1:8080/book/ISBN6379;echo ;done
{"method":"GET"}
{"method":"POST"}
{"method":"PUT"}
{"method":"DELETE"}

1.2. Gin渲染

所谓渲染即生成响应体,在后端开发中,常用的响应体有 json,html,protobuf,静态文件等。

1.2.1. Json 渲染

func main() {
	r := gin.Default()

	// 使用 gin.H{} 构造json
	r.GET("/", func(c *gin.Context) {
		c.JSON(200, gin.H{
			"method":   c.Request.Method,
			"location": c.Request.URL.String(),
			"message":  "root page",
		})
	})

	// 使用结构体
	r.GET("/index", func(c *gin.Context) {
		res := struct {
			Method   string `json:"method"`
			Location string `json:"location"`
			Message  string `json:"message"`
		}{c.Request.Method, c.Request.URL.Path, "index page"}

		c.JSON(200, &res)
	})

	err := r.Run("0.0.0.0:8080")
	if err != nil {
		log.Fatal("Init gin server failed,err:%s", err.Error())
	}
}
[root@duduniao ~]# curl http://127.0.0.1:8080
{"location":"/","message":"root page","method":"GET"}
[root@duduniao ~]# curl http://127.0.0.1:8080/index
{"method":"GET","location":"/index","message":"index page"}

1.2.2. YAML渲染

func main() {
	r := gin.Default()

	// 使用 gin.H{} 构造json
	r.GET("/", func(c *gin.Context) {
		c.YAML(200, gin.H{
			"method":   c.Request.Method,
			"location": c.Request.URL.String(),
			"message":  "root page",
		})
	})

	// 使用结构体
	r.GET("/index", func(c *gin.Context) {
		res := struct {
			Method   string
			Location string
			Message  string
		}{c.Request.Method, c.Request.URL.Path, "index page"}

		c.YAML(200, &res)
	})

	err := r.Run("0.0.0.0:8080")
	if err != nil {
		log.Fatal("Init gin server failed,err:%s", err.Error())
	}
}
[root@duduniao ~]# curl http://127.0.0.1:8080/index
method: GET
location: /index
message: index page
[root@duduniao ~]# curl http://127.0.0.1:8080
location: /
message: root page
method: GET

1.2.3. XML渲染

与json和yaml返回不同,XML不支持返回匿名结构体对象

func main() {
	r := gin.Default()

	// 使用 gin.H{} 构造json
	r.GET("/", func(c *gin.Context) {
		c.XML(200, gin.H{
			"method":   c.Request.Method,
			"location": c.Request.URL.String(),
			"message":  "root page",
		})
	})

	// 使用结构体
	r.GET("/index", func(c *gin.Context) {
		// 使用匿名结构体会无法显示
		type msg struct {
			Method   string
			Location string
			Message  string
		}
		var res = msg{c.Request.Method, c.Request.URL.Path, "index page"}
		c.XML(200, &res)
	})

	err := r.Run("0.0.0.0:8080")
	if err != nil {
		log.Fatal("Init gin server failed,err:%s", err.Error())
	}
}
[root@duduniao ~]# curl http://127.0.0.1:8080/index
<msg><Method>GET</Method><Location>/index</Location><Message>index page</Message></msg>
[root@duduniao ~]# curl http://127.0.0.1:8080
<map><method>GET</method><location>/</location><message>root page</message></map>

1.2.4. HTML渲染

在做web开发时,返回渲染的 html 页面常见很常见,一般流程: 在 templates 目录下创建html模板文件 --> 导入html模板文件 --> 执行 c.HTML()渲染

[root@duduniao gin_basic]# tree
.
├── html.go
└── templates
    ├── book
    │   └── index.html
    └── user
        └── index.html
{{define "user/index.html"}}
<!DOCTYPE html>
<html lang="en">
<body>
<h1>{{.name}}</h1>
<h1>{{.age}}</h1>
</body>
</html>
{{end}}
{{define "book/index.html"}}
<!DOCTYPE html>
<html lang="en">
<body>
<h2>{{.title}}</h2>
</body>
</html>
{{end}}
package main

import (
	"github.com/gin-gonic/gin"
	log "github.com/sirupsen/logrus"
)

func main() {
	r := gin.Default()
	r.LoadHTMLGlob("templates/**/*.html")

	r.GET("/book/", func(c *gin.Context) {
		c.HTML(200, "book/index.html", gin.H{"title": "books"})
	})

	r.GET("/user/", func(c *gin.Context) {
		c.HTML(200, "user/index.html", gin.H{"name":"张三", "age":18})
	})

	err := r.Run("0.0.0.0:8080")
	if err != nil {
		log.Fatal("Init gin server failed,err:%s", err.Error())
	}
}

1.3. 请求处理

1.3.1. PATH参数处理

所谓PATH参数解析,就是指解析URL的locaction,如从请求 GET /student/201909011208 获取学生的ID 201909011208。PATH中关键词解析是通过占位符来实现的。

func main()  {
	r := gin.Default()

	r.GET("/user/search/:name/:age", func(c *gin.Context) {
		name, age := c.Param("name"), c.Param("age")
		c.JSON(200, gin.H{
			"name": name,
			"age": age,
		})
	})

	if err := r.Run("0.0.0.0:80"); err != nil {
		log.Fatal("Init web failed,err:%s", err.Error())
	}
}
[root@duduniao ~]# curl http://127.0.0.1/user/search/zhangsan/20
{"age":"20","name":"zhangsan"}
[root@duduniao ~]# curl http://127.0.0.1/user/search/张三/20
{"age":"20","name":"张三"}

1.3.2. GET请求参数

在非 RESTAPI 请求中,请求基本都是通过GET加URL参数完成的,比如 GET /user/search?name=zhangsan&age=20 ,因此对请求URL中的参数解析场景很常见,Gin中采用 c.Query(key)  和 c.DefaultQuery(key,default) 获取,如果对于的key不存在,前者返回空字符串,后者范围指定的default。

func main() {
	r := gin.Default()

	r.GET("/user/search", func(c *gin.Context) {
		name := c.Query("name")
		age := c.Query("age")
		class := c.DefaultQuery("class", "101") // 如果不存在则使用默认值
		c.JSON(200, gin.H{
			"name":  name,
			"age":   age,
			"class": class,
		})
	})

	if err := r.Run("0.0.0.0:80"); err != nil {
		log.Fatal("Init web failed,err:%s", err.Error())
	}
}
[root@duduniao ~]# curl "http://127.0.0.1/user/search?name=zhangsan&age=20"
{"age":"20","class":"101","name":"zhangsan"}
[root@duduniao ~]# curl "http://127.0.0.1/user/search?name=zhangsan"
{"age":"","class":"101","name":"zhangsan"}

1.3.3. 获取表单参数

在网站中开发中,用户提交注册或者登陆信息一般通过POST表单进行传递,Gin中获取该参数的值是通过 c.PostForm(key) 实现。

func main() {
	r := gin.Default()

	r.POST("/user/search", func(c *gin.Context) {
		name := c.PostForm("name")
		age := c.PostForm("age")
		class := c.PostForm("class")
		c.JSON(200, gin.H{
			"name":  name,
			"age":   age,
			"class": class,
		})
	})

	if err := r.Run("0.0.0.0:80"); err != nil {
		log.Fatal("Init web failed,err:%s", err.Error())
	}
}
[root@duduniao ~]# curl -X POST "http://127.0.0.1/user/search" -d "name=zhangsan&age=19"
{"age":"19","class":"","name":"zhangsan"}
[root@duduniao ~]# curl -X POST "http://127.0.0.1/user/search" -d "name=zhangsan&age=19&class=301"
{"age":"19","class":"301","name":"zhangsan"}

1.3.4. 获取json参数

获取GET请求参数和表单参数有快捷方式,如上面两个案例所示,但是json类的请求体需要使用 c.BindJson(&obj)  来取参数。

func main() {
	r := gin.Default()

	r.POST("/user/search", func(c *gin.Context) {
		var user UserInfo
		err := c.BindJSON(&user)  // 绑定json解析,query,yaml,xml同理
		if err != nil {
			log.Errorf("Get args failed, err:%s\n", err.Error())
			c.JSON(200, gin.H{"err": err.Error()})
			return
		}
		c.JSON(200, gin.H{
			"name":  user.Name,
			"age":   user.Age,
			"class": user.Class,
		})
	})

	if err := r.Run("0.0.0.0:80"); err != nil {
		log.Fatal("Init web failed,err:", err.Error())
	}
}
[root@duduniao ~]# curl -X POST -H "Content-Type:application/json" -d '{"name":"zhangsan","age":20}' http://127.0.0.1/user/search
{"age":20,"class":"","name":"zhangsan"}
[root@duduniao ~]# curl -X POST -H "Content-Type:application/json" -d '{"name":"zhangsan","age":20,"class":"312"}' http://127.0.0.1/user/search
{"age":20,"class":"312","name":"zhangsan"}

1.3.5. 参数自动绑定

上面的案例中,都是明确知道是URL传参,或者POST表单,或者Json请求体。但是Gin中还提供了一种自动判断参数类型,并解析到结构体对象中的方法。可以自动根据请求类型、Content-Type 来判断请求数据的类型,解析后绑定到指定的结构体中。需要注意的是,结构体中必须打上 form 的tag,否则GET请求中的参数无法解析成功

type UserInfo struct {
	Name  string `form:"name" json:"name"`
	Age   uint8  `form:"age" json:"age"`
	Class string `form:"class" json:"class"`
}

func search(c *gin.Context) {
	var user UserInfo
	err := c.ShouldBind(&user) // 根据 Content-Type 自动进行解析
	if err != nil {
		log.Errorf("Get args failed, err:%s\n", err.Error())
		c.JSON(200, gin.H{"err": err.Error()})
		return
	}
	c.JSON(200, gin.H{
		"method": c.Request.Method,
		"name":   user.Name,
		"age":    user.Age,
		"class":  user.Class,
	})
}

func main() {
	r := gin.Default()

	r.POST("/user/search", search)
	r.GET("/user/search", search)

	if err := r.Run("0.0.0.0:80"); err != nil {
		log.Fatal("Init web failed,err:", err.Error())
	}
}
[root@duduniao ~]# curl "http://127.0.0.1/user/search?name=zhangsan&age=20&class=301"
{"age":20,"class":"301","method":"GET","name":"zhangsan"}
[root@duduniao ~]# curl -X POST "http://127.0.0.1/user/search" -d "name=zhangsan&age=19&class=301"
{"age":19,"class":"301","method":"POST","name":"zhangsan"}
[root@duduniao ~]# curl -X POST -H "Content-Type:application/json" -d '{"name":"zhangsan","age":20,"class":"312"}' http://127.0.0.1/user/search
{"age":20,"class":"312","method":"POST","name":"zhangsan"}

1.4. 上传的文件处理

1.4.1. 上传单个文件

func main() {
	r := gin.Default()

	r.POST("/upload/", func(c *gin.Context) {
		file, err := c.FormFile("background")  // 上传单个文件,明确知晓文件的 key 
		if err != nil {
			c.JSON(400, gin.H{"status": 411, "msg": "recv file failed,err" + err.Error()})
			log.Errorf("recv file failed,err:%s\n", err.Error())
			return
		}
		if err := c.SaveUploadedFile(file, file.Filename); err != nil {
			c.JSON(400, gin.H{"status": 412, "msg": "save file failed, err" + err.Error()})
			log.Errorf("save %s failed,err:%s\n", file.Filename, err.Error())
			return
		}
		log.Infof("recv %s success\n", file.Filename)
		c.JSON(200, gin.H{"status": 200, "msg": "revc success"})
	})

	if err := r.Run("0.0.0.0:80"); err != nil {
		log.Fatal("Init web failed,err:", err.Error())
	}
}
[root@duduniao 壁纸]# curl -X POST -F 'background=@2020-03-14_22-40-07.jpg'  http://127.0.0.1/upload/
{"msg":"revc success","status":200}

1.4.2. 上传多个文件

func main() {
	r := gin.Default()
	// 设置能接收的缓冲区大小
	r.MaxMultipartMemory = 50 * 1024 * 1024
	r.POST("/upload/", func(c *gin.Context) {
		form, err := c.MultipartForm() // 获取表单信息
		if err != nil {
			c.JSON(400, gin.H{"status":411,"msg": "recv file failed, err:" + err.Error()})
			log.Errorf("get files failed,err:%s\n",err.Error())
			return
		}
		files, ok := form.File["files"]  // 获取表单中的文件列表
		if !ok {
			c.JSON(400, gin.H{"status":412,"msg":"recv file failed, err: no key names files"})
			log.Errorf("get files failed,err:%s\n","no key names files")
			return
		}

		// 变量文件对象并写入本地
		for index, file := range files {
			if err := c.SaveUploadedFile(file, fmt.Sprintf("%d-%s", index, file.Filename)); err != nil {
				c.JSON(400, gin.H{"status":412,"msg":"save file failed, err:" + err.Error()})
				log.Errorf("save %s failed,err:%s\n",file.Filename, err.Error())
				return
			}
			log.Infof("save %s success\n",file.Filename)
		}
		log.Infof("recv %s success\n", "file.Filename")
		c.JSON(200, gin.H{"status":200,"msg":"revc success"})
	})

	if err := r.Run("0.0.0.0:80"); err != nil {
		log.Fatal("Init web failed,err:", err.Error())
	}
}
[root@duduniao 壁纸]# curl -X POST -F 'files=@2020-03-10_10-06-06.jpg' -F 'files=@2020-03-10_10-09-32.jpg' -F 'files=@2020-03-14_22-41-02.jpg'  http://127.0.0.1/upload/
{"msg":"revc success","status":200}

1.5. 重定向

在HTTP协议中,重定向分为两种: 301 永久重定向,302 临时重定向。在Gin中,重定向有两种实现的方式,一种是是通过HTTP重定向,指定返回码(301,302)和目标地址即可;另一种是通过修改路由实现,内部进行URL跳转,类似于nginx的rewrite规则。

1.5.1. HTTP重定向

func main() {
	r := gin.Default()

	r.GET("/book/", func(c *gin.Context) {
        // c.Redirect(301, "/books/")  // 永久重定向
		c.Redirect(302, "/books/")  // 临时重定向
	})
	r.GET("/books/", func(c *gin.Context) {
		c.JSON(200, gin.H{
			"method" : c.Request.Method,
			"location": c.Request.URL.String(),
			"message": "Hello world!",
		})
	})

	err := r.Run("0.0.0.0:8080")
	if err != nil {
		log.Fatal("Init gin server failed,err:%s", err.Error())
	}
}
[root@duduniao ~]# curl -Lv http://127.0.0.1:8080/book/
......
> GET /book/ HTTP/1.1
> Host: 127.0.0.1:8080
> User-Agent: curl/7.58.0
> Accept: */*
>
< HTTP/1.1 302 Found
< Content-Type: text/html; charset=utf-8
< Location: /books/
< Date: Sat, 13 Jun 2020 13:52:54 GMT
< Content-Length: 30
<
......
> GET /books/ HTTP/1.1
> Host: 127.0.0.1:8080
> User-Agent: curl/7.58.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Content-Type: application/json; charset=utf-8
< Date: Sat, 13 Jun 2020 13:52:54 GMT
< Content-Length: 62
<
* Connection #0 to host 127.0.0.1 left intact
{"location":"/books/","message":"Hello world!","method":"GET"}

1.5.2. 路由重定向

func main() {
	r := gin.Default()

	r.GET("/book/", func(c *gin.Context) {
		c.Request.URL.Path = "/books/"
		r.HandleContext(c)
	})
	r.GET("/books/", func(c *gin.Context) {
		c.JSON(200, gin.H{
			"method" : c.Request.Method,
			"location": c.Request.URL.String(),
			"message": "Hello world!",
		})
	})

	err := r.Run("0.0.0.0:8080")
	if err != nil {
		log.Fatal("Init gin server failed,err:%s", err.Error())
	}
}

[root@duduniao ~]# curl -Lv http://127.0.0.1:8080/book/
*   Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to 127.0.0.1 (127.0.0.1) port 8080 (#0)
> GET /book/ HTTP/1.1
> Host: 127.0.0.1:8080
> User-Agent: curl/7.58.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Content-Type: application/json; charset=utf-8
< Date: Sat, 13 Jun 2020 13:57:18 GMT
< Content-Length: 62
<
* Connection #0 to host 127.0.0.1 left intact
{"location":"/books/","message":"Hello world!","method":"GET"}

1.6. Gin路由

路由就是请求(请求方法+URL)和处理函数之间的关系绑定,根据业务场景,可以分为普通路由和路由组,普通路由就是一个一个零散的路由,路由组是若干个有关联关系的路由组成,比如对同一个URL的不同方法,同一个API版本的所有路由集合等,路由组习惯性一对{}包裹同组的路由,这只是为了看着清晰,你用不用{}包裹功能上没什么区别。

1.6.1. 普通路由

r.GET("/index", func(c *gin.Context) {...})
r.PUT("/index", func(c *gin.Context) {...})
r.POST("/index", func(c *gin.Context) {...})

r.Any("/index", func(c *gin.Context) {...})  // 匹配任意方法
r.NoRoute(func(c *gin.Context) {...})        // 没有任何匹配项

1.6.2. 路由组

userGroup := r.Group("/user")
{
    userGroup.GET("/index", func(c *gin.Context) {...})
    userGroup.GET("/login", func(c *gin.Context) {...})
    userGroup.POST("/login", func(c *gin.Context) {...})

}

shopGroup := r.Group("/shop")
{
    shopGroup.GET("/index", func(c *gin.Context) {...})
    shopGroup.GET("/cart", func(c *gin.Context) {...})
    shopGroup.POST("/checkout", func(c *gin.Context) {...})
}

1.7. 中间件

Gin框架允许开发者在处理请求的过程中,加入用户自己的钩子(Hook)函数。这个钩子函数就叫中间件,中间件适合处理一些公共的业务逻辑,比如登录认证、权限校验、数据分页、记录日志、耗时统计等。

1.7.1. 定义中间件

Gin中的中间件必须是一个gin.HandlerFunc类型的函数,因此定义中间件就是定义返回值为 gin.HandlerFunc 的函数。中间件类似于一个装饰器,其中 c.Next() 表示执行被装饰的函数,即路由处理函数。

func timer() gin.HandlerFunc {
	return func(c *gin.Context) {
		start := time.Now()
		c.Next()  // 执行
		log.Infof("The request [%s %s] const %v\n", c.Request.Method, c.Request.URL.Path, time.Since(start))
	}
}

1.7.2. 注册中间件

中间件的注册分为三种类型:

  • 注册全局中间件,即所有的请求都会调用该中间件进行装饰: r.Use(timer()) 
  • 为某个特定路由注册,支持多个中间件: r.GET("/", timer(), func(c *gin.context) {} ) 
  • 为路由组注册中间件,可以在定义路由组的时候注册: userGroup := r.Group("/user", timer2()) 

也可以对定义好的路由组添加中间件: deployGroup.Use(timer2()) 
全局中间件和路由中间件,执行顺序,参考一下案例:

import (
	"fmt"
	"github.com/gin-gonic/gin"
	log "github.com/sirupsen/logrus"
	"time"
)

func response(c *gin.Context) {
	time.Sleep(time.Millisecond * 100)
	c.JSON(200, gin.H{"message": "ok", "location": c.Request.URL.String()})
}

func timer1() gin.HandlerFunc {
	return func(c *gin.Context) {
		fmt.Println("timer1 start")
		start := time.Now()
		c.Next()
		log.Infof("[timer1] The request [%s %s] const %v\n", c.Request.Method, c.Request.URL.Path, time.Since(start))
		fmt.Println("timer1 stop")
	}
}

func timer2() gin.HandlerFunc {
	return func(c *gin.Context) {
		fmt.Println("timer2 start")
		start := time.Now()
		c.Next()
		log.Infof("[timer2] The request [%s %s] const %v\n", c.Request.Method, c.Request.URL.Path, time.Since(start))
		fmt.Println("timer2 stop")
	}
}

func main() {
	r := gin.Default()

	r.Use(timer1()) // 注册全局中间件

	r.GET("/index", timer2(), response) // 为特定路由注册中间件

	userGroup := r.Group("/user", timer2()) // 为路由组添加中间件
	{
		userGroup.GET("/index", response)
		userGroup.GET("/edit", response)
	}

	deployGroup := r.Group("/deploy")
	deployGroup.Use(timer2()) // 为路由组添加中间件
	{
		deployGroup.GET("/deployment", response)
		deployGroup.GET("/service", response)
		deployGroup.GET("/template", response)
	}

	if err := r.Run("0.0.0.0:80"); err != nil {
		log.Fatalf("Init gin failed, err:%s\n", err.Error())
	}
}

[root@duduniao ~]# curl http://127.0.0.1/index
{"location":"/index","message":"ok"}
------
timer1 start
timer2 start
INFO[0009] [timer2] The request [GET /index] const 100.7118ms
timer2 stop
INFO[0009] [timer1] The request [GET /index] const 100.8172ms
timer1 stop

[root@duduniao ~]# curl http://127.0.0.1/user/edit
{"location":"/user/edit","message":"ok"}
------
timer1 start
timer2 start
INFO[0018] [timer2] The request [GET /user/edit] const 100.5929ms
timer2 stop
INFO[0018] [timer1] The request [GET /user/edit] const 100.6653ms
timer1 stop

[root@duduniao ~]# curl http://127.0.0.1/deploy/service
{"location":"/deploy/service","message":"ok"}
------
timer1 start
timer2 start
INFO[0026] [timer2] The request [GET /deploy/service] const 100.3718ms
timer2 stop
INFO[0026] [timer1] The request [GET /deploy/service] const 100.4596ms
timer1 stop

1.7.3. 中间件注意事项

  • gin默认中间件

gin.Default()默认使用了Logger和Recovery中间件,其中:

  • Logger中间件将日志写入gin.DefaultWriter,即使配置了GIN_MODE=release。
  • Recovery中间件会recover任何panic。如果有panic的话,会写入500响应码。

如果不想使用上面两个默认的中间件,可以使用gin.New()新建一个没有任何默认中间件的路由。

  • gin中间件中使用goroutine

当在中间件或handler中启动新的goroutine时,不能使用原始的上下文(c *gin.Context),必须使用其只读副本(c.Copy())