Gin是一个用Go语言编写的web框架。它是一个类似于martini但拥有更好性能的API框架, 由于使用了httprouter,速度提高了近40倍。 如果你是性能和高效的追求者, 你会爱上Gin

一、Gin框架介绍

Go世界里最流行的Web框架,Github上有32K+star。 基于httprouter开发的Web框架。 中文文档齐全,简单易用的轻量级框架。

二、Gin框架安装与使用

2.1 安装

下载并安装Gin:

  1. go get -u github.com/gin-gonic/gin

若遇到protobuf缺失(Google相关的包):— 未开启go mod的情况下

Logger是负责进行打印并输出日志的中间件,方便开发者进行程序调试;Recovery中间件的作用是如果程序执行过程中遇到panic中断了服务,则Recovery会恢复程序执行,并返回服务器500内部错误。通常情况下,我们使用默认的gin.Default创建Engine实例。 注意:Run方法的参数是冒号+port的字符串!!!

  1. package main
  2. import (
  3. "github.com/gin-gonic/gin"
  4. )
  5. func main() {
  6. // 创建一个默认的路由引擎
  7. r := gin.Default()
  8. // GET:请求方式;/hello:请求的路径
  9. // 当客户端以GET方法请求/hello路径时,会执行后面的匿名函数
  10. r.GET("/hello", func(c *gin.Context) {
  11. // c.JSON:返回JSON格式的数据
  12. c.JSON(200, gin.H{
  13. "message": "Hello world!",
  14. })
  15. })
  16. // 启动HTTP服务,默认在0.0.0.0:8080启动服务
  17. r.Run(":8000")
  18. }

将上面的代码保存并编译执行,然后使用浏览器打开127.0.0.1:8080/hello就能看到一串JSON字符串。

2.3 通用的处理方法: r.Handle()方法处理请求

engine中可以直接进行HTTP请求的处理,在engine中使用Handle方法进行http请求的处理。Handle方法包含三个参数,具体如下所示:

  1. func (group *RouterGroup) Handle(httpMethod, relativePath string, handlers ...HandlerFunc) IRoutes
  2. // 例子
  3. engine.Handle("GET", "/hello", func(context *gin.Context) {
  4. //获取请求接口
  5. fmt.Println(context.FullPath())
  6. //获取字符串参数
  7. name := context.DefaultQuery("name", "")
  8. fmt.Println(name)
  9. //输出
  10. context.Writer.Write([]byte("Hello ," + name))
  11. })
  12. // Context是gin框架中封装的一个结构体,这是gin框架中最重要,最基础的一个结构体对象。该结构体可以提供我们操作请求,处理请求,获取数据等相关的操作,通常称之为上下文对象,简单说为我们提供操作环境。
  • httpMethod:第一个参数表示要处理的HTTP的请求类型,是GET、POST、DELETE等8种请求类型中的一种。

注意: httpMethod不支持小写,仅全部大写

  • relativePath:第二个参数表示要解析的接口,由开发者进行定义。
  • handlers:第三个参数是处理对应的请求的代码的定义。

    三、规范之RESTful API

    REST与技术无关,代表的是一种软件架构风格,REST是Representational State Transfer的简称,中文翻译为“表征状态转移”或“表现层状态转化”。
    推荐阅读阮一峰 理解RESTful架构
    简单来说,REST的含义就是客户端与Web服务器之间进行交互的时候,使用HTTP协议中的4个请求方法代表不同的动作。

  • GET用来获取资源

  • POST用来新建资源
  • PUT用来更新资源
  • DELETE用来删除资源。
  • OPTIONS、HEAD、TRACE、CONNECT 这几种不常用

只要API程序遵循了REST风格,那就可以称其为RESTful API。目前在前后端分离的架构中,前后端基本都是通过RESTful API来进行交互。
例如,我们现在要编写一个管理书籍的系统,我们可以查询对一本书进行查询、创建、更新和删除等操作,我们在编写程序的时候就要设计客户端浏览器与我们Web服务端交互的方式和路径。按照经验我们通常会设计成如下模式:

请求方法 URL 含义
GET /book 查询书籍信息
POST /create_book 创建书籍记录
POST /update_book 更新书籍信息
POST /delete_book 删除书籍信息

同样的需求我们按照RESTful API设计如下:

请求方法 URL 含义
GET /book 查询书籍信息
POST /book 创建书籍记录
PUT /book 更新书籍信息
DELETE /book 删除书籍信息

Gin框架支持开发RESTful API的开发。

  1. func main() {
  2. r := gin.Default()
  3. r.GET("/book", func(c *gin.Context) {
  4. c.JSON(200, gin.H{
  5. "message": "GET",
  6. })
  7. })
  8. r.POST("/book", func(c *gin.Context) {
  9. c.JSON(200, gin.H{
  10. "message": "POST",
  11. })
  12. })
  13. r.PUT("/book", func(c *gin.Context) {
  14. c.JSON(200, gin.H{
  15. "message": "PUT",
  16. })
  17. })
  18. r.DELETE("/book", func(c *gin.Context) {
  19. c.JSON(200, gin.H{
  20. "message": "DELETE",
  21. })
  22. })
  23. }

开发RESTful API的时候我们通常使用Postman来作为客户端的测试工具。

四、Gin渲染

4.1 HTML渲染 - LoadHTMLGlob()或者LoadHTMLFiles()

我们首先定义一个存放模板文件的templates文件夹,然后在其内部按照业务分别定义一个posts文件夹和一个users文件夹。 posts/index.html文件的内容如下:

  1. {{define "posts/index.html"}}
  2. <!DOCTYPE html>
  3. <html lang="en">
  4. <head>
  5. <meta charset="UTF-8">
  6. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  7. <meta http-equiv="X-UA-Compatible" content="ie=edge">
  8. <title>posts/index</title>
  9. </head>
  10. <body>
  11. {{.title}}
  12. </body>
  13. </html>
  14. {{end}}

users/index.html文件的内容如下:

  1. {{define "users/index.html"}}
  2. <!DOCTYPE html>
  3. <html lang="en">
  4. <head>
  5. <meta charset="UTF-8">
  6. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  7. <meta http-equiv="X-UA-Compatible" content="ie=edge">
  8. <title>users/index</title>
  9. </head>
  10. <body>
  11. {{.title}}
  12. </body>
  13. </html>
  14. {{end}}

Gin框架中使用LoadHTMLGlob()或者LoadHTMLFiles()方法进行HTML模板渲染。

  1. func main() {
  2. r := gin.Default()
  3. r.LoadHTMLGlob("templates/**/*");// 多个
  4. //r.LoadHTMLFiles("templates/posts/index.html","templates/users/index.html")// 单个文件
  5. r.GET("/posts/index", func(c *gin.Context) {
  6. c.HTML(http.StatusOK, "posts/index.html", gin.H{
  7. "title": "posts/index",
  8. })
  9. })
  10. r.GET("users/index", func(c *gin.Context) {
  11. c.HTML(http.StatusOK, "users/index.html", gin.H{
  12. "title": "users/index",
  13. })
  14. })
  15. r.Run(":8080")
  16. }

4.2 自定义模板函数 - SetFuncMap

定义一个不转义相应内容的safe模板函数如下:

  1. func main() {
  2. router := gin.Default()
  3. router.SetFuncMap(template.FuncMap{
  4. "safe": func(str string) template.HTML{
  5. return template.HTML(str)
  6. },
  7. })
  8. router.LoadHTMLFiles("./index.tmpl")
  9. router.GET("/index", func(c *gin.Context) {
  10. c.HTML(http.StatusOK, "index.tmpl", "<a href='https://liwenzhou.com'>李文周的博客</a>")
  11. })
  12. router.Run(":8080")
  13. }

index.tmpl中使用定义好的safe模板函数:

  1. <!DOCTYPE html>
  2. <html lang="zh-CN">
  3. <head>
  4. <title>修改模板引擎的标识符</title>
  5. </head>
  6. <body>
  7. <div>{{ . | safe }}</div>
  8. </body>
  9. </html>

4.3 静态文件处理 - Static

当我们渲染的HTML文件中引用了静态文件时,我们只需要按照以下方式在渲染页面前调用gin.Static方法即可。

  1. func main() {
  2. r := gin.Default()
  3. r.Static("/static", "./static")
  4. r.LoadHTMLGlob("templates/**/*")
  5. // ...
  6. r.Run(":8080")
  7. }

4.4 使用模板继承

Gin框架默认都是使用单模板,如果需要使用block template功能,可以通过"github.com/gin-contrib/multitemplate"库实现,具体示例如下:
首先,假设我们项目目录下的templates文件夹下有以下模板文件,其中home.tmplindex.tmpl继承了base.tmpl

  1. templates
  2. ├── includes
  3. ├── home.tmpl
  4. └── index.tmpl
  5. ├── layouts
  6. └── base.tmpl
  7. └── scripts.tmpl

然后我们定义一个loadTemplates函数如下:

  1. func loadTemplates(templatesDir string) multitemplate.Renderer {
  2. r := multitemplate.NewRenderer()
  3. layouts, err := filepath.Glob(templatesDir + "/layouts/*.tmpl")
  4. if err != nil {
  5. panic(err.Error())
  6. }
  7. includes, err := filepath.Glob(templatesDir + "/includes/*.tmpl")
  8. if err != nil {
  9. panic(err.Error())
  10. }
  11. // 为layouts/和includes/目录生成 templates map
  12. for _, include := range includes {
  13. layoutCopy := make([]string, len(layouts))
  14. copy(layoutCopy, layouts)
  15. files := append(layoutCopy, include)
  16. r.AddFromFiles(filepath.Base(include), files...)
  17. }
  18. return r
  19. }

我们在main函数中

  1. func indexFunc(c *gin.Context){
  2. c.HTML(http.StatusOK, "index.tmpl", nil)
  3. }
  4. func homeFunc(c *gin.Context){
  5. c.HTML(http.StatusOK, "home.tmpl", nil)
  6. }
  7. func main(){
  8. r := gin.Default()
  9. r.HTMLRender = loadTemplates("./templates")
  10. r.GET("/index", indexFunc)
  11. r.GET("/home", homeFunc)
  12. r.Run()
  13. }

4.5 补充文件路径处理

关于模板文件和静态文件的路径,我们需要根据公司/项目的要求进行设置。可以使用下面的函数获取当前执行程序的路径。

  1. func getCurrentPath() string {
  2. if ex, err := os.Executable(); err == nil {
  3. return filepath.Dir(ex)
  4. }
  5. return "./"
  6. }

4.6 JSON渲染

  1. func main() {
  2. r := gin.Default()
  3. // gin.H 是map[string]interface{}的缩写
  4. r.GET("/someJSON", func(c *gin.Context) {
  5. // 方式一:自己拼接JSON
  6. c.JSON(http.StatusOK, gin.H{"message": "Hello world!"})
  7. })
  8. r.GET("/moreJSON", func(c *gin.Context) {
  9. // 方法二:使用结构体
  10. var msg struct {
  11. Name string `json:"user"` // json中key值使用`user`
  12. Message string
  13. Age int
  14. }
  15. msg.Name = "小王子"
  16. msg.Message = "Hello world!"
  17. msg.Age = 18
  18. c.JSON(http.StatusOK, msg)
  19. })
  20. r.Run(":8080")
  21. }

4.7 XML渲染

注意需要使用具名的结构体类型。

  1. func main() {
  2. r := gin.Default()
  3. // gin.H 是map[string]interface{}的缩写
  4. r.GET("/someXML", func(c *gin.Context) {
  5. // 方式一:自己拼接JSON
  6. c.XML(http.StatusOK, gin.H{"message": "Hello world!"})
  7. })
  8. r.GET("/moreXML", func(c *gin.Context) {
  9. // 方法二:使用结构体
  10. type MessageRecord struct {
  11. Name string
  12. Message string
  13. Age int
  14. }
  15. var msg MessageRecord
  16. msg.Name = "小王子"
  17. msg.Message = "Hello world!"
  18. msg.Age = 18
  19. c.XML(http.StatusOK, msg)
  20. })
  21. r.Run(":8080")
  22. }

4.8 YMAL渲染

  1. r.GET("/someYAML", func(c *gin.Context) {
  2. c.YAML(http.StatusOK, gin.H{"message": "ok", "status": http.StatusOK})
  3. })

4.9 protobuf渲染

  1. r.GET("/someProtoBuf", func(c *gin.Context) {
  2. reps := []int64{int64(1), int64(2)}
  3. label := "test"
  4. // protobuf 的具体定义写在 testdata/protoexample 文件中。
  5. data := &protoexample.Test{
  6. Label: &label,
  7. Reps: reps,
  8. }
  9. // 请注意,数据在响应中变为二进制数据
  10. // 将输出被 protoexample.Test protobuf 序列化了的数据
  11. c.ProtoBuf(http.StatusOK, data)
  12. })

五、获取参数

5.1 获取querystring参数 - QueryDefaultQuery

querystring指的是URL中?后面携带的参数,例如:/user/search?username=小王子&address=沙河。 获取请求的querystring参数的方法如下:

  1. func main() {
  2. //Default返回一个默认的路由引擎
  3. r := gin.Default()
  4. r.GET("/user/search", func(c *gin.Context) {
  5. username := c.DefaultQuery("username", "小王子"); // 设置默认值
  6. //username := c.Query("username")
  7. address := c.Query("address")
  8. //输出json结果给调用方
  9. c.JSON(http.StatusOK, gin.H{
  10. "message": "ok",
  11. "username": username,
  12. "address": address,
  13. })
  14. })
  15. r.Run()
  16. }

5.2 获取form参数 - PostFormDefaultPostForm

请求的数据通过form表单来提交,例如向/user/search发送一个POST请求,获取请求数据的方式如下:

  1. func main() {
  2. //Default返回一个默认的路由引擎
  3. r := gin.Default()
  4. r.POST("/user/search", func(c *gin.Context) {
  5. // DefaultPostForm取不到值时会返回指定的默认值
  6. // 1.默认值
  7. id := c.DefaultPostForm("id", "10086")
  8. // 2.仅获取
  9. username := c.PostForm("username")
  10. username := c.PostForm("username")
  11. fmt.Println(username)
  12. // 3.返回的第二个参数表示是否获取到
  13. password, exist := c.GetPostForm("password")
  14. if exist {
  15. fmt.Println(password)
  16. }
  17. //输出json结果给调用方
  18. c.JSON(http.StatusOK, gin.H{
  19. "message": "ok",
  20. "id": id,
  21. "username": username,
  22. "password": password,
  23. "address": address,
  24. })
  25. })
  26. r.Run(":8080")
  27. }

5.3 路由参数-获取path参数 - Param

express路由
请求的参数通过URL路径传递,例如:/user/search/小王子/沙河。 获取请求URL路径中的参数的方式如下。

  1. func main() {
  2. //Default返回一个默认的路由引擎
  3. r := gin.Default()
  4. r.GET("/user/search/:username/:address", func(c *gin.Context) {
  5. username := c.Param("username") // 都是必传,不然url路径不全
  6. address := c.Param("address")
  7. //输出json结果给调用方
  8. c.JSON(http.StatusOK, gin.H{
  9. "message": "ok",
  10. "username": username,
  11. "address": address,
  12. })
  13. })
  14. r.Run(":8080")
  15. }

5.4 ** 参数绑定 - .ShouldBind() 反射

为了能够更方便的获取请求相关参数,提高开发效率,我们可以基于请求的Content-Type识别请求数据类型并利用反射机制自动提取请求中QueryStringform表单JSONXML等参数到结构体中。 下面的示例代码演示了.ShouldBind()强大的功能,它能够基于请求自动提取JSONform表单QueryString类型的数据,并把值绑定到指定的结构体对象。
相对于ShouldBind根据Content-type自动解析;但是ShouldBindQueryBindJSONShouldBindJSON等以ShouldXxx或者BindXxx解析特定格式的参数

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

ShouldBind会按照下面的顺序解析请求中的数据完成绑定:

  1. 如果是 GET 请求,只使用 Form 绑定引擎(query)。
  2. 如果是 POST 请求,首先检查 content-type 是否为 JSONXML,然后再使用 Formform-data)。

    六、文件上传

    6.1 单个文件上传

    文件上传前端页面代码:
    1. <!DOCTYPE html>
    2. <html lang="zh-CN">
    3. <head>
    4. <title>上传文件示例</title>
    5. </head>
    6. <body>
    7. <form action="/upload" method="post" enctype="multipart/form-data">
    8. <input type="file" name="f1" />
    9. <input type="submit" value="上传" />
    10. </form>
    11. </body>
    12. </html>
    后端gin框架部分代码:
    1. func main() {
    2. router := gin.Default()
    3. // 处理multipart forms提交文件时默认的内存限制是32 MiB
    4. // 可以通过下面的方式修改
    5. // router.MaxMultipartMemory = 8 << 20 // 8 MiB
    6. router.POST("/upload", func(c *gin.Context) {
    7. // 单个文件
    8. file, err := c.FormFile("f1"); // 参数'f1'是前端input的name属性值
    9. if err != nil {
    10. c.JSON(http.StatusInternalServerError, gin.H{
    11. "message": err.Error(),
    12. })
    13. return
    14. }
    15. log.Println(file.Filename)
    16. dst := fmt.Sprintf("C:/tmp/%s", file.Filename); //写入保存路径
    17. // 上传文件到指定的目录
    18. c.SaveUploadedFile(file, dst)
    19. c.JSON(http.StatusOK, gin.H{
    20. "message": fmt.Sprintf("'%s' uploaded!", file.Filename),
    21. })
    22. })
    23. router.Run()
    24. }

    6.2 多个文件上传

    1. func main() {
    2. router := gin.Default()
    3. // 处理multipart forms提交文件时默认的内存限制是32 MiB
    4. // 可以通过下面的方式修改
    5. // router.MaxMultipartMemory = 8 << 20 // 8 MiB
    6. router.POST("/upload", func(c *gin.Context) {
    7. // Multipart form 多个文件!!!
    8. form, _ := c.MultipartForm()
    9. files := form.File["file"]
    10. for index, file := range files {
    11. log.Println(file.Filename)
    12. dst := fmt.Sprintf("C:/tmp/%s_%d", file.Filename, index)
    13. // 上传文件到指定的目录
    14. c.SaveUploadedFile(file, dst)
    15. }
    16. c.JSON(http.StatusOK, gin.H{
    17. "message": fmt.Sprintf("%d files uploaded!", len(files)),
    18. })
    19. })
    20. router.Run()
    21. }

    七、重定向

    HTTP重定向

    HTTP 重定向很容易。 内部、外部重定向均支持。
    1. r.GET("/test", func(c *gin.Context) {
    2. c.Redirect(http.StatusMovedPermanently, "http://www.sogo.com/")
    3. })

    路由重定向

    路由重定向,使用HandleContext
    1. r.GET("/test", func(c *gin.Context) {
    2. // 指定重定向的URL
    3. c.Request.URL.Path = "/test2"
    4. r.HandleContext(c)
    5. })
    6. r.GET("/test2", func(c *gin.Context) {
    7. c.JSON(http.StatusOK, gin.H{"hello": "world"})
    8. })

    八、Gin路由

    8.1 普通路由

    1. r.GET("/index", func(c *gin.Context) {...})
    2. r.GET("/login", func(c *gin.Context) {...})
    3. r.POST("/login", func(c *gin.Context) {...})
    此外,还有一个可以匹配所有请求方法的Any方法如下:
    1. r.Any("/test", func(c *gin.Context) {...})
    为没有配置处理函数的路由添加处理程序,默认情况下它返回404代码,下面的代码为没有匹配到路由的请求都返回views/404.html页面。
    1. r.NoRoute(func(c *gin.Context) {
    2. c.HTML(http.StatusNotFound, "views/404.html", nil)
    3. })

    8.2 路由组-Group

    我们可以将拥有共同URL前缀的路由划分为一个路由组。习惯性一对{}包裹同组的路由,这只是为了看着清晰,你用不用{}包裹功能上没什么区别。不要理解成函数的{}
    1. func main() {
    2. r := gin.Default()
    3. userGroup := r.Group("/user")
    4. {
    5. userGroup.GET("/index", func(c *gin.Context) {...})
    6. userGroup.GET("/login", func(c *gin.Context) {...})
    7. userGroup.POST("/login", func(c *gin.Context) {...})
    8. }
    9. shopGroup := r.Group("/shop")
    10. {
    11. shopGroup.GET("/index", func(c *gin.Context) {...})
    12. shopGroup.GET("/cart", func(c *gin.Context) {...})
    13. shopGroup.POST("/checkout", func(c *gin.Context) {...})
    14. }
    15. r.Run()
    16. }
    路由组也是支持嵌套的,例如:
    1. shopGroup := r.Group("/shop")
    2. {
    3. shopGroup.GET("/index", func(c *gin.Context) {...})
    4. shopGroup.GET("/cart", func(c *gin.Context) {...})
    5. shopGroup.POST("/checkout", func(c *gin.Context) {...})
    6. // 嵌套路由组
    7. xx := shopGroup.Group("xx")
    8. xx.GET("/oo", func(c *gin.Context) {...})
    9. }
    通常我们将路由分组用在划分业务逻辑或划分API版本时。

    8.3 路由原理 - 树

    Gin框架中的路由使用的是httprouter这个库。
    其基本原理就是构造一个路由地址的前缀树。

    九、Gin中间件

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

在gin中,中间件称之为middleware,中间件的类型定义如下所示:

  1. // HandlerFunc defines the handler used by gin middleware as return value.
  2. type HandlerFunc func(*Context)

HandlerFunc是一个函数类型,接收一个Context参数。用于编写程序处理函数并返回HandleFunc类型,作为中间件的定义。

9.0 中间件Use用法

在之前学习的课程中,均使用gin.Default创建了gin引擎engins变量,其中,就使用了中间件。如下图所示:

  1. func Default() *Engine {
  2. debugPrintWARNINGDefault()
  3. engine := New()
  4. engine.Use(Logger(), Recovery())
  5. return engine
  6. }
  7. //Log中间件
  8. func Logger() HandlerFunc {
  9. return LoggerWithConfig(LoggerConfig{})
  10. }
  11. //Recovery中间件
  12. func Recovery() HandlerFunc {
  13. return RecoveryWithWriter(DefaultErrorWriter)
  14. }

在Default函数中,engine调用Use方法设置了Logger中间件和Recovery中间件。Use函数接收一个可变参数(三个…),类型为HandlerFunc,恰为中间件的类型。Use方法定义如下:

  1. func (engine *Engine) Use(middleware ...HandlerFunc) IRoutes {
  2. engine.RouterGroup.Use(middleware...)
  3. engine.rebuild404Handlers()
  4. engine.rebuild405Handlers()
  5. return engine
  6. }

9.1 定义中间件[必须为 gin.HandlerFunc类型]

Gin中的中间件必须是一个gin.HandlerFunc类型。例如我们像下面的代码一样定义一个统计请求耗时的中间件。

  1. // StatCost 是一个统计耗时请求耗时的中间件
  2. func StatCost() gin.HandlerFunc {
  3. return func(c *gin.Context) {
  4. start := time.Now()
  5. c.Set("name", "小王子") // 可以通过c.Set在请求上下文中设置值,后续的处理函数能够取到该值
  6. // 调用该请求的剩余处理程序
  7. c.Next()
  8. // 不调用该请求的剩余处理程序
  9. // c.Abort()
  10. // 计算耗时
  11. cost := time.Since(start)
  12. log.Println(cost)
  13. }
  14. }

9.2 注册中间件

在gin框架中,我们可以为每个路由添加任意数量的中间件。

为全局路由注册 - Use

  1. func main() {
  2. // 新建一个没有任何默认中间件的路由
  3. r := gin.New()
  4. // 注册一个全局中间件
  5. r.Use(StatCost())
  6. r.GET("/test", func(c *gin.Context) {
  7. name := c.MustGet("name").(string) // 从上下文取值
  8. log.Println(name)
  9. c.JSON(http.StatusOK, gin.H{
  10. "message": "Hello world!",
  11. })
  12. })
  13. r.Run()
  14. }

为某个路由单独注册 - 参数

  1. // 给/test2路由单独注册中间件(可注册多个)
  2. r.GET("/test2", StatCost(), func(c *gin.Context) {
  3. name := c.MustGet("name").(string) // 从上下文取值
  4. log.Println(name)
  5. c.JSON(http.StatusOK, gin.H{
  6. "message": "Hello world!",
  7. })
  8. })

为路由组注册中间件 - 参数

为路由组注册中间件有以下两种写法。
写法1:

  1. shopGroup := r.Group("/shop", StatCost())
  2. {
  3. shopGroup.GET("/index", func(c *gin.Context) {...})
  4. ...
  5. }

写法2:

  1. shopGroup := r.Group("/shop")
  2. shopGroup.Use(StatCost())
  3. {
  4. shopGroup.GET("/index", func(c *gin.Context) {...})
  5. ...
  6. }

9.3 中间件注意事项

gin默认中间件

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

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

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

gin中间件中使用goroutine,启用新线程时:

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

十、运行多个服务

我们可以在多个端口启动服务,例如:

  1. package main
  2. import (
  3. "log"
  4. "net/http"
  5. "time"
  6. "github.com/gin-gonic/gin"
  7. "golang.org/x/sync/errgroup"
  8. )
  9. var (
  10. g errgroup.Group
  11. )
  12. func router01() http.Handler {// 服务1!!!
  13. e := gin.New()
  14. e.Use(gin.Recovery())
  15. e.GET("/", func(c *gin.Context) {
  16. c.JSON(
  17. http.StatusOK,
  18. gin.H{
  19. "code": http.StatusOK,
  20. "error": "Welcome server 01",
  21. },
  22. )
  23. })
  24. return e
  25. }
  26. func router02() http.Handler {// 服务2!!!
  27. e := gin.New()
  28. e.Use(gin.Recovery())
  29. e.GET("/", func(c *gin.Context) {
  30. c.JSON(
  31. http.StatusOK,
  32. gin.H{
  33. "code": http.StatusOK,
  34. "error": "Welcome server 02",
  35. },
  36. )
  37. })
  38. return e
  39. }
  40. func main() {
  41. server01 := &http.Server{
  42. Addr: ":8080",
  43. Handler: router01(),
  44. ReadTimeout: 5 * time.Second,
  45. WriteTimeout: 10 * time.Second,
  46. }
  47. server02 := &http.Server{
  48. Addr: ":8081",
  49. Handler: router02(),
  50. ReadTimeout: 5 * time.Second,
  51. WriteTimeout: 10 * time.Second,
  52. }
  53. // 借助errgroup.Group或者自行开启两个goroutine分别启动两个服务
  54. g.Go(func() error {
  55. return server01.ListenAndServe()
  56. })
  57. g.Go(func() error {
  58. return server02.ListenAndServe()
  59. })
  60. if err := g.Wait(); err != nil {
  61. log.Fatal(err)
  62. }
  63. }