【go语言学习】web开发框架gin

一、gin简介

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

二、gin安装和使用

安装

  1. 下载并安装 gin:
$ go get -u github.com/gin-gonic/gin

2、将gin引入到项目中:

import "github.com/gin-gonic/gin"

3、如果使用诸如 http.StatusOK 之类的常量,则需要引入 net/http 包:

import "net/http"

使用

// main.go
package main

import (
        "net/http"

        "github.com/gin-gonic/gin"
)

func main() {
        // 创建一个默认的路由引擎
        r := gin.Default()
        // 当客户端以GET方法请求/路径时,会执行后面的匿名函数
        r.GET("/", func(c *gin.Context) {
                // 返回json格式的数据
                c.JSON(http.StatusOK, gin.H{
                        "message": "hello world",
                })
        })
        // 监听并在 0.0.0.0:8080 上启动服务
        r.Run()
}

然后,执行 go run main.go 命令来运行代码,并且在浏览器中访问 0.0.0.0:8080/,页面显示:

{"message":"hello world"}

三、RESTful API

REST与技术无关,代表的是一种软件架构风格,REST是Representational State Transfer的简称,中文翻译为“表征状态转移”或“表现层状态转化”。

简单来说,REST的含义就是客户端与Web服务器之间进行交互的时候,使用HTTP协议中的4个请求方法代表不同的动作。

  • GET用来获取资源
  • POST用来新建资源
  • PUT用来更新资源
  • DELETE用来删除资源。

只要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删除书籍

示例代码:

package main

import (
        "net/http"

        "github.com/gin-gonic/gin"
)

func main() {
        r := gin.Default()
        r.GET("/book", func(c *gin.Context) {
                c.JSON(http.StatusOK, gin.H{
                        "message": "GET",
                })
        })
        r.POST("/book", func(c *gin.Context) {
                c.JSON(http.StatusOK, gin.H{
                        "message": "POST",
                })
        })
        r.PUT("/book", func(c *gin.Context) {
                c.JSON(http.StatusOK, gin.H{
                        "message": "PUT",
                })
        })
        r.DELETE("/book", func(c *gin.Context) {
                c.JSON(http.StatusOK, gin.H{
                        "message": "DELETE",
                })
        })
        r.Run()
}

四、HTML渲染

1、模板解析与渲染

使用 LoadHTMLGlob () 或者 LoadHTMLFiles ()

package main

import (
        "net/http"

        "github.com/gin-gonic/gin"
)

func main() {
        r := gin.Default()
        // r.LoadHTMLFiles("templates/posts/index.tmpl", "templates/users/index.tmpl")
        r.LoadHTMLGlob("templates/**/*")
        r.GET("/posts/index", func(c *gin.Context) {
                c.HTML(http.StatusOK, "posts/index.tmpl", gin.H{
                        "tittle": "posts",
                })
        })
        r.GET("/users/index", func(c *gin.Context) {
                c.HTML(http.StatusOK, "users/index.tmpl", gin.H{
                        "tittle": "users",
                })
        })
        r.Run()
}

templates/posts/index.tmpl

{{ define "posts/index.tmpl" }}
<!DOCTYPE html>
<html >
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    {{ .tittle }}
</body>
</html>
{{ end }}

templates/users/index.tmpl

{{ define "users/index.tmpl" }}
<!DOCTYPE html>
<html >
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    {{ .tittle }}
</body>
</html>
{{ end }}
2、自定义分隔符
r.Delims("{[", "]}")
3、自定义模板函数
package main

import (
        "fmt"
        "html/template"
        "net/http"
        "time"

        "github.com/gin-gonic/gin"
)

func formatAsDate(t time.Time) string {
        year, month, day := t.Date()
        return fmt.Sprintf("%d-%02d-%02d", year, month, day)
}
func now() string {
        year, month, day := time.Now().Date()
        return fmt.Sprintf("%d年%02d月%02d日", year, month, day)
}
func main() {
        r := gin.Default()
        r.SetFuncMap(template.FuncMap{
                "formatAsDate": formatAsDate,
                "now":          now,
        })
        r.LoadHTMLFiles("index.tmpl")
        r.GET("/", func(c *gin.Context) {
                c.HTML(http.StatusOK, "index.tmpl", gin.H{
                        "now": time.Now(),
                })
        })
        r.Run()
}
4、静态文件处理
package main

import (
        "net/http"

        "github.com/gin-gonic/gin"
)

func main() {
        router := gin.Default()
        router.Static("/statics", "./statics/")
        router.LoadHTMLFiles("index.tmpl")
        router.GET("/index", func(c *gin.Context) {
                c.HTML(http.StatusOK, "index.tmpl", nil)
        })
        router.Run()
}
<!-- index.tmpl -->
<!DOCTYPE html>
<html >
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link rel="stylesheet" href="/statics/css/main.css">
    <title>Document</title>
</head>
<body>
    <h2>hello world</h2>
    <img src="/statics/pictures/10.jpg" alt="">
    <script src="/statics/js/main.js"></script>
</body>
</html>

五、获取参数

Gin框架将处理HTTP请求参数以及如何响应等操作都封装到了gin.Conetxt结构体,并为gin.Context提供了非常多的方法,因此了解gin.Context的结构定义与方法,对使用Gin框架编写Web项目非常重要。

// gin.Context
type Context struct {
    Request *http.Request
    Writer  ResponseWriter
    Params Params
    // Keys is a key/value pair exclusively for the context of each request.
    Keys map[string]interface{}
    // Errors is a list of errors attached to all the handlers/middlewares who used this context.
    Errors errorMsgs
    // Accepted defines a list of manually accepted formats for content negotiation.
    Accepted []string
    // contains filtered or unexported fields
}

从上面的gin.Context的结构定义来看,gin.Context封装了http.Request和http.ResponseWriter。

1、获取路径中的参数(path)

path是指请求的url中域名之后从/开始的部分,如访问web地址:https://localhost:8080/user/jack/user/jack部分便是path,可以使用gin.Context中提供的方法获取这部分参数。

获取路径中的参数有两种方法:

  • 使用gin.Context的中Param()方法获取path中的参数
  • 使用gin.Context中的Params字段获取path中的参数
func (c *Context) Param(key string) string {}
type Params []Param
func (ps Params) ByName(name string) (va string) {}
func (ps Params) Get(name string) (string, bool) {}

示例代码:

package main

import (
        "net/http"

        "github.com/gin-gonic/gin"
)

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

        // 此规则能够匹配/user/john这种格式,但不能匹配/user/ 或 /user这种格式
        router.GET("/user/:name", func(c *gin.Context) {
                name := c.Param("name")
                c.String(http.StatusOK, "Hello %s", name)
        })

        // 但是,这个规则既能匹配/user/john/格式也能匹配/user/john/send这种格式
        // 如果没有其他路由器匹配/user/john,它将重定向到/user/john/
        router.GET("/user/:name/*action", func(c *gin.Context) {
                name := c.Param("name")
                action := c.Param("action")
                message := name + " is " + action
                c.String(http.StatusOK, message)
        })
        router.GET("/user/:id", func(c *gin.Context) {
                id, _ := c.Params.Get("id")
                // id := c.Params.ByName("id")
                c.JOSN(http.StatusOK, gin.H{
                        "id": id,
                })
        })
        router.Run()
}
2、获取get请求参数(query)

query是指url请求地址中的问号后面的部,称为查询参数。如https://localhost:8080/index?name=jack&id=100name=jack&id=100就是查询参数。

gin.Context提供了以下几个方法,用于获取Query部分的参数:

  • 获取单个参数
func (c *Context) GetQuery(key string) (string, bool) {}
func (c *Context) Query(key string) string {}
func (c *Context) DefaultQuery(key, defaultValue string) string {}

上面三个方法用于获取单个数值,GetQuery比Query多返回一个error类型的参数,实际上Query方法只是封装了GetQuery方法,并忽略GetQuery方法返回的错误而已,而DefaultQuery方法则在没有获取相应参数值的返回一个默认值。

  • 获取数组
func (c *Context) GetQueryArray(key string) ([]string, bool) {}
func (c *Context) QueryArray(key string) []string {}
  • 获取map
func (c *Context) QueryMap(key string) map[string]string {}
func (c *Context) GetQueryMap(key string) (map[string]string, bool) {}

示例代码:

package main

import (
        "net/http"

        "github.com/gin-gonic/gin"
)

func main() {
        router := gin.Default()
        router.GET("/index", func(c *gin.Context) {
                // name, _ := c.GetQuery("name")
                name := c.Query("name")
                id := c.DefaultQuery("id", "0000")
                c.String(http.StatusOK, "Hello, name:%s, id:%v", name, id)
        })
        router.GET("/user", func(c *gin.Context) {
                // ids, _ := c.GetQueryArray("id")
                ids := c.QueryArray("id")
                c.JSON(http.StatusOK, gin.H{
                        "ids": ids,
                })
        })
        router.GET("/article", func(c *gin.Context) {
                article := c.QueryMap("articles")
                c.JSON(http.StatusOK, article)
        })
        router.Run()
}

请求:http://localhost:8080/index?name=jack&id=100

响应:Hello, name:jack, id:100

请求:http://localhost:8080/user?id=10&id=20&id=40

响应:{"ids":["10","20","40"]}

请求:http://localhost:8080/article?articles[tittle]=golang

响应:{"tittle":"golang"}

3、获取post请求参数(body)

一般HTTP的Post请求参数都是通过body部分传给服务器端的,尤其是数据量大或安全性要求较高的数据,如登录功能中的账号密码等参数。

gin.Context提供了以下四个方法让我们获取body中的数据,不过要说明的是,下面的四个方法,只能获取Content-type是application/x-www-form-urlencoded或multipart/form-data时body中的数据。

示例代码:

func (c *Context) PostForm(key string) string {}
func (c *Context) PostFormArray(key string) []string {}
func (c *Context) PostFormMap(key string) map[string]string {}
func (c *Context) DefaultPostForm(key, defaultValue string) string {}
func (c *Context) GetPostForm(key string) (string, bool) {}
func (c *Context) GetPostFormArray(key string) ([]string, bool) {}
func (c *Context) GetPostFormMap(key string) (map[string]string, bool) {}
func (c *Context) GetRawData() ([]byte, error) {}
package main

import (
        "net/http"

        "github.com/gin-gonic/gin"
)

func main() {
        router := gin.Default()
        router.LoadHTMLFiles("index.tmpl")
        router.GET("/index", func(c *gin.Context) {
                c.HTML(http.StatusOK, "index.tmpl", nil)
        })
        router.POST("/index", func(c *gin.Context) {
                username := c.PostForm("username")
                password := c.PostForm("password")
                gender := c.DefaultPostForm("gender", "male")
                c.JSON(http.StatusOK, gin.H{
                        "username": username,
                        "password": password,
                        "gender":   gender,
                })
        })
        router.Run()
}
<!-- index.tmpl -->
<!DOCTYPE html>
<html >
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <form action="/index", method="POST">
        <input type="text", name="username"><br>
        <input type="password", name="password"><br>
        <input type="radio", name="gender" value="male">male
        <input type="radio", name="gender" value="female">female <br>
        <input type="submit" value="提交">
    </form>
</body>
</html>

六、数据绑定

我们直接使用gin.Context提供的方法获取请求中通过path、query、body带上来的参数,但使用前面的那些方法,并不能处理请求中比较复杂的数据结构,比如Content-type为application/json或application/xml时,其所带上的数据会很复杂,因此我们需要使用另外一种方法获取这些数据,这种方式叫数据绑定。

Gin框架将数据绑定的操作都封装在gin/binding这个包中,下面是gin/binding包定义的常量,说明gin/binding包所支持的Content-type格式。

const (
    MIMEJSON              = "application/json"
    MIMEHTML              = "text/html"
    MIMEXML               = "application/xml"
    MIMEXML2              = "text/xml"
    MIMEPlain             = "text/plain"
    MIMEPOSTForm          = "application/x-www-form-urlencoded"
    MIMEMultipartPOSTForm = "multipart/form-data"
    MIMEPROTOBUF          = "application/x-protobuf"
    MIMEMSGPACK           = "application/x-msgpack"
    MIMEMSGPACK2          = "application/msgpack"
    MIMEYAML              = "application/x-yaml"
)

gin.binding包也定义处理不同Content-type提交数据的处理结构体,并以变量的形式让其他包可以访问,如下:

var (
    JSON          = jsonBinding{}
    XML           = xmlBinding{}
    Form          = formBinding{}
    Query         = queryBinding{}
    FormPost      = formPostBinding{}
    FormMultipart = formMultipartBinding{}
    ProtoBuf      = protobufBinding{}
    MsgPack       = msgpackBinding{}
    YAML          = yamlBinding{}
    Uri           = uriBinding{}
)

但实际上,我们并不需要调用gin/binding包的代码来完成数据绑定的功能,因为gin.Context中已经在gin.Context的基础上封装了许多更加快捷的方法供我们使用,gin提供了两套绑定方法:

  • Must bind

    Methods方法:Bind, BindJSON, BindXML, BindQuery, BindYAML

    Behavior行为:这些方法底层使用 MustBindWith,如果存在绑定错误,请求将被中止,返回http状态为400的响应给客户端。

  • Should bind

    Methods方法:ShouldBind, ShouldBindJSON, ShouldBindXML, ShouldBindQuery, ShouldBindYAML

    Behavior行为:这些方法底层使用 ShouldBindWith,如果存在绑定错误,则返回错误,开发人员可以正确处理请求和错误。

1、以Bind为前缀的系列方法
  • Path
func (c *Context) BindUri(obj interface{}) error {}
  • Query
func (c *Context) BindQuery(obj interface{}) error {}
  • Body

当我们在HTTP请求中Body设置不同数据格式,需要设置相应头部Content-Type的值,比较常用的为json、xml、yaml,gin.Context提供下面三个方法绑定对应Content-type时body中的数据。

func (c *Context) BindJSON(obj interface{}) error {}
func (c *Context) BindXML(obj interface{}) error {]
func (c *Context) BindYAML(obj interface{}) error {}

除了上面三个方法外,更常用的Bind()方法,Bind()方法会自动根据Content-Type的值选择不同的绑定类型。

func (c *Context) Bind(obj interface{}) error {}

上面几个方法都是获取固定Content-type或自动根据Content-type选择绑定类型,我们也可以使用下面两个方法自行选择绑定类型。

// 第二个参数值是gin.binding中定义好的常量
func (c *Context) BindWith(obj interface{}, b binding.Binding) error {}
func (c *Context) MustBindWith(obj interface{}, b binding.Binding) error {}
2、以ShouldBind为前缀的系列方法
  • Path
func (c *Context) ShouldBindUri(obj interface{}) error {}
  • Query
func (c *Context) ShouldBindQuery(obj interface{}) error {}
  • Body
func (c *Context) ShouldBind(obj interface{}) error {}
func (c *Context) ShouldBindJSON(obj interface{}) error {}
func (c *Context) ShouldBindXML(obj interface{}) error {}
func (c *Context) ShouldBindYAML(obj interface{}) error {}
func (c *Context) ShouldBindBodyWith(obj interface{}, bb  binding.BindingBody) (err error) {}
func (c *Context) ShouldBindWith(obj interface{}, b binding.Binding) error {}

示例代码:

// main.go
package main

import (
        "net/http"

        "github.com/gin-gonic/gin"
)

// User 结构体
type User struct {
        Username string   `form:"username" json:"username" uri:"username" binding:"required"`
        Passwrod string   `form:"password" json:"password" uri:"password" binding:"required"`
        Hobbys   []string `form:"hobbys" json:"bobbys" uri:"hobbys" binding:"required"`
}

func main() {
        router := gin.Default()
        router.LoadHTMLFiles("register.tmpl")
        // Path
        router.GET("/user/:username/:password", func(c *gin.Context) {
                var user User
                c.ShouldBindUri(&user)
                c.JSON(http.StatusOK, user)
        })
        // Query
        router.GET("/index", func(c *gin.Context) {
                var user User
                c.ShouldBind(&user)
                c.JSON(http.StatusOK, user)
        })
        // Body
        router.GET("/register", func(c *gin.Context) {
                c.HTML(http.StatusOK, "register.tmpl", nil)
        })
        router.POST("/register", func(c *gin.Context) {
                var user User
                c.ShouldBind(&user)
                c.JSON(http.StatusOK, user)
        })
        router.Run()
}
<!-- register.tmpl -->
<!DOCTYPE html>
<html >
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <form action="/register" method="POST", enctype="multipart/form-data">
        username: <input type="text" name="username"><br>
        <p></p>
        password: <input type="password" name="password"><br>
        <p></p>
        hobbys: <input type="checkbox" name="hobbys" value="football">football
        <input type="checkbox" name="hobbys" value="basketball">basketball
        <input type="checkbox" name="hobbys" value="volleyball">volleyball<br>
        <p></p>
        <input type="submit" value="register">
    </form> 
</body>
</html>

请求:http://localhost:8080/user/jack/123456

响应:{"username":"jack","password":"123456","bobbys":null}

请求:http://localhost:8080/index?username=jack&password=123456

响应:{"username":"jack","password":"123456","bobbys":null}

请求:http://localhost:8080/register 输入username和password、hobbys,提交

响应:{"username":"jack","password":"123456","bobbys":["football","basketball"]}

七、文件上传

1、单文件上传
package main

import (
        "fmt"
        "math/rand"
        "net/http"
        "path/filepath"
        "time"

        "github.com/gin-gonic/gin"
)

func main() {
        router := gin.Default()
        router.LoadHTMLFiles("index.tmpl")
        router.GET("/index", func(c *gin.Context) {
                c.HTML(http.StatusOK, "index.tmpl", nil)
        })
        router.POST("/upload", func(c *gin.Context) {
                file, _ := c.FormFile("file")
                rand.Seed(time.Now().UnixNano())
                fileName := fmt.Sprintf("%d%d%s", time.Now().Unix(), rand.Intn(99999-10000)+10000, filepath.Ext(file.Filename))
                dst := "./upload/" + fileName
                fmt.Println(dst)
                c.SaveUploadedFile(file, dst)
                c.JSON(http.StatusOK, gin.H{
                        "message":  "uploaded",
                        "fileName": fileName,
                })
        })
        router.Run()
}
2、多文件上传
package main

import (
        "fmt"
        "math/rand"
        "net/http"
        "path/filepath"
        "time"

        "github.com/gin-gonic/gin"
)

func main() {
        router := gin.Default()
        router.LoadHTMLFiles("index.tmpl")
        router.GET("/index", func(c *gin.Context) {
                c.HTML(http.StatusOK, "index.tmpl", nil)
        })
        router.POST("/upload", func(c *gin.Context) {
                form, _ := c.MultipartForm()
                files := form.File["file"]
                fmt.Println(files)
                rand.Seed(time.Now().UnixNano())
                for _, file := range files {
                        fileName := fmt.Sprintf("%d%d%s", time.Now().Unix(), rand.Intn(99999-10000)+10000, filepath.Ext(file.Filename))
                        dst := "./upload/" + fileName
                        fmt.Println(dst)
                        c.SaveUploadedFile(file, dst)
                }
                c.JSON(http.StatusOK, gin.H{
                        "message": "uploaded",
                        "file":    files,
                })
        })
        router.Run()
}

八、重定向

package main

import (
        "net/http"

        "github.com/gin-gonic/gin"
)

func main() {
        router := gin.Default()
        // 外部链接重定向
        router.GET("/index", func(c *gin.Context) {
                c.Redirect(http.StatusMovedPermanently, "http://www.baidu.com")
        })
        // 内部路由重定向
        router.GET("/home", func(c *gin.Context) {
                c.Request.URL.Path = "/"
                router.HandleContext(c)
        })
        router.GET("/", func(c *gin.Context) {
                c.String(http.StatusOK, "hello world")
        })
        router.Run()
}

九、gin路由

1、普通路由
router.GET("/index", func(c *gin.Context) {...})
router.GET("/login", func(c *gin.Context) {...})
router.POST("/login", func(c *gin.Context) {...})

还有一个可以匹配所有请求方法的Any方法如下:

router.Any("/test", func(c *gin.Context) {...})

为没有配置处理函数的路由添加处理程序,默认情况下它返回404代码,下面的代码为没有匹配到路由的请求都返回views/404.html页面。

router.NoRoute(func(c *gin.Context) {
        c.HTML(http.StatusNotFound, "views/404.html", nil)
})
2、路由组

我们可以将拥有共同URL前缀的路由划分为一个路由组。习惯性一对{}包裹同组的路由,这只是为了看着清晰,你用不用{}包裹功能上没什么区别。

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

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

路由组也是支持嵌套的,例如:

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) {...})
                // 嵌套路由组
                xx := shopGroup.Group("xx")
                xx.GET("/oo", func(c *gin.Context) {...})
        }

十、gin中间件

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

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

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

1、内置中间件

Gin内置一些中间件,我们可以直接使用,下面是内置中间件列表:

func BasicAuth(accounts Accounts) HandlerFunc {}
func BasicAuthForRealm(accounts Accounts, realm string) HandlerFunc {}
func Bind(val interface{}) HandlerFunc {} //拦截请求参数并进行绑定
func ErrorLogger() HandlerFunc {}       //错误日志处理
func ErrorLoggerT(typ ErrorType) HandlerFunc {} //自定义类型的错误日志处理
func Logger() HandlerFunc {} //日志记录
func LoggerWithConfig(conf LoggerConfig) HandlerFunc {}
func LoggerWithFormatter(f LogFormatter) HandlerFunc {}
func LoggerWithWriter(out io.Writer, notlogged ...string) HandlerFunc {}
func Recovery() HandlerFunc {}
func RecoveryWithWriter(out io.Writer) HandlerFunc {}
func WrapF(f http.HandlerFunc) HandlerFunc {} //将http.HandlerFunc包装成中间件
func WrapH(h http.Handler) HandlerFunc {} //将http.Handler包装成中间件
2、自定义中间件

Gin中的中间件必须是一个gin.HandlerFunc类型。

// gin
type HandlerFunc func(*Context)

(1)定义一个gin.HandleFunc类型的函数作为中间件:

示例代码:

package main

import (
        "fmt"
        "net/http"
        "time"

        "github.com/gin-gonic/gin"
)

// StatCost 是一个计算耗时的中间件
func StatCost(c *gin.Context) {
        // 传递数据
        c.Set("name", "jack")
        start := time.Now()
        // 调用该请求的剩余处理程序
        c.Next()
        // 不调用该请求的剩余处理程序
        // c.Abort()
        // 计算耗时
        cost := time.Since(start)
        fmt.Println(cost)
}

func main() {
        router := gin.Default()
        // 为/路由注册中间件StatCost
        router.GET("/", StatCost, func(c *gin.Context) {
                // 获取中间件传递的数据
                name := c.MustGet("name").(string)
                c.JSON(http.StatusOK, gin.H{
                        "name": name,
                })
        })
        router.Run()
}

(2)通过自定义方法,返回一个中间件函数,这是Gin框架中更常用的方式:

示例代码:

//定义一个返回中间件的方法
func MyMiddleware(){
    //自定义逻辑
    
    //返回中间件
    return func(c *gin.Context){
        //中间件逻辑
    }
}
3、注册中间件

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

  • 全局使用中间件

直拉使用 gin.Engine 结构体的 Use() 方法便可以在所有请求应用中间件,这样做,中间件便会在全局起作用。

router.Use(gin.Recovery())//在全局使用内置中间件
  • 为某个路由单独注册

单个请求路由,也可以应用中间件,如下:

router := gin.New()
router.GET("/test",gin.Recovery(),gin.Logger(),func(c *gin.Context){
    c.JSON(200,"test")
})
  • 为路由组注册中间件

根据业务不同划分不同 路由分组(RouterGroup ),不同的路由分组再应用不同的中间件,这样就达到了不同的请求由不同的中间件进行拦截处理。

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

routerGroup := router.Group("/", MyMiddleware)
{
        routerGroup.GET("/user", func(c *gin.Context){})
        routerGroup.POST("/user", func(c *gin.Context){})
        ...
}
routerGroup := router.Group("/")
routerGroup.Use(MyMiddleware)
{
        routerGroup.GET("/user", func(c *gin.Context){})
        routerGroup.POST("/user", func(c *gin.Context){})
        ...
}
4、中间件使用

(1)gin默认中间件

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

Logger中间件将日志写入gin.DefaultWriter,即使配置了GIN_MODE=release。

Recovery中间件会recover任何panic。如果有panic的话,会写入500响应码。

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

(2)数据传递

当我们在中间件拦截并预先处理好数据之后,要如何将数据传递我们定义的处理请求的HTTP方法呢?可以使用 gin.Context 中的 Set() 方法,其定义如下, Set() 通过一个key来存储作何类型的数据,方便下一层处理方法获取。

func (c *Context) Set(key string, value interface{})

当我们在中间件中通过Set方法设置一些数值,在下一层中间件或HTTP请求处理方法中,可以使用下面列出的方法通过key获取对应数据。

其中,gin.Context的Get方法返回 interface{} ,通过返回exists可以判断key是否存在。

func (c *Context) Get(key string) (value interface{}, exists bool)

当我们确定通过Set方法设置对应数据类型的值时,可以使用下面方法获取应数据类型的值。

func (c *Context) GetBool(key string) (b bool)
func (c *Context) GetDuration(key string) (d time.Duration)
func (c *Context) GetFloat64(key string) (f64 float64)
func (c *Context) GetInt(key string) (i int)
func (c *Context) GetInt64(key string) (i64 int64)
func (c *Context) GetString(key string) (s string)
func (c *Context) GetStringMap(key string) (sm map[string]interface{})
func (c *Context) GetStringMapString(key string) (sms map[string]string)
func (c *Context) GetStringMapStringSlice(key string) (smss map[string][]string)
func (c *Context) GetStringSlice(key string) (ss []string)
func (c *Context) GetTime(key string) (t time.Time)

(3)拦截请求与后置拦截

  • 拦截请求

中间件的最大作用就是拦截过滤请求,比如我们有些请求需要用户登录或者需要特定权限才能访问,这时候便可以中间件中做过滤拦截,当用户请求不合法时,可以使用下面列出的 gin.Context 的几个方法中断用户请求:

下面三个方法中断请求后,直接返回200,但响应的body中不会有数据。

func (c *Context) Abort()
func (c *Context) AbortWithError(code int, err error) *Error
func (c *Context) AbortWithStatus(code int)

使用AbortWithStatusJSON()方法,中断用户请求后,则可以返回 json格式的数据.

func (c *Context) AbortWithStatusJSON(code int, jsonObj interface{})
  • 后置拦截

前面我们讲的都是到达我们定义的HTTP处理方法前进行拦截,其实,如果在中间件中调用 gin.Context 的 Next() 方法,则可以请求到达并完成业务处理后,再经过中间件后置拦截处理, Next() 方法定义如下:

func (c *Context) Next()

在中间件调用 Next() 方法, Next() 方法之前的代码会在到达请求方法前执行, Next() 方法之后的代码则在请求方法处理后执行:

func MyMiddleware(c *gin.Context){
    //请求前
    c.Next()
    //请求后
}

(4)gin中间件中使用goroutine

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