22

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

 3 years ago
source link: https://studygolang.com/articles/31194
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

一、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="/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())。

有疑问加站长微信联系

iiUfA3j.png!mobile

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK