【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 lang="en">
<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 lang="en">
<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 lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link rel="stylesheet" href="//www.greatytc.com/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 lang="en">
<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 lang="en">
<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())。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,968评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,601评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,220评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,416评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,425评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,144评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,432评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,088评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,586评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,028评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,137评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,783评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,343评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,333评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,559评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,595评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,901评论 2 345