【go語(yǔ)言學(xué)習(xí)】web開發(fā)框架gin

一、gin簡(jiǎn)介

Gin 是一個(gè)用 Go (Golang) 編寫的 HTTP web 框架。 它是一個(gè)類似于 martini 但擁有更好性能的 API 框架,由于 httprouter,速度提高了近 40 倍,是最快的 http 路由器和框架。 如果你是性能和高效的追求者,你會(huì)愛上 Gin。

二、gin安裝和使用

安裝

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

2、將gin引入到項(xiàng)目中:

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() {
    // 創(chuàng)建一個(gè)默認(rèn)的路由引擎
    r := gin.Default()
    // 當(dāng)客戶端以GET方法請(qǐng)求/路徑時(shí),會(huì)執(zhí)行后面的匿名函數(shù)
    r.GET("/", func(c *gin.Context) {
        // 返回json格式的數(shù)據(jù)
        c.JSON(http.StatusOK, gin.H{
            "message": "hello world",
        })
    })
    // 監(jiān)聽并在 0.0.0.0:8080 上啟動(dòng)服務(wù)
    r.Run()
}

然后,執(zhí)行 go run main.go 命令來(lái)運(yùn)行代碼,并且在瀏覽器中訪問(wèn) 0.0.0.0:8080/,頁(yè)面顯示:

{"message":"hello world"}

三、RESTful API

REST與技術(shù)無(wú)關(guān),代表的是一種軟件架構(gòu)風(fēng)格,REST是Representational State Transfer的簡(jiǎn)稱,中文翻譯為“表征狀態(tài)轉(zhuǎn)移”或“表現(xiàn)層狀態(tài)轉(zhuǎn)化”。

簡(jiǎn)單來(lái)說(shuō),REST的含義就是客戶端與Web服務(wù)器之間進(jìn)行交互的時(shí)候,使用HTTP協(xié)議中的4個(gè)請(qǐng)求方法代表不同的動(dòng)作。

  • GET用來(lái)獲取資源
  • POST用來(lái)新建資源
  • PUT用來(lái)更新資源
  • DELETE用來(lái)刪除資源。

只要API程序遵循了REST風(fēng)格,那就可以稱其為RESTful API。目前在前后端分離的架構(gòu)中,前后端基本都是通過(guò)RESTful API來(lái)進(jìn)行交互。

例如,我們現(xiàn)在要編寫一個(gè)管理書籍的系統(tǒng),我們可以查詢對(duì)一本書進(jìn)行查詢、創(chuàng)建、更新和刪除等操作,我們?cè)诰帉懗绦虻臅r(shí)候就要設(shè)計(jì)客戶端瀏覽器與我們Web服務(wù)端交互的方式和路徑。按照經(jīng)驗(yàn)我們通常會(huì)設(shè)計(jì)成如下模式:

請(qǐng)求方法 URL 動(dòng)作
GET /book 查詢書籍
POST /create_book 添加書籍
POST /update_book 更新書籍
POST /delete_book 刪除書籍

同樣的需求我們按照RESTful API設(shè)計(jì)如下:

請(qǐng)求方法 URL 動(dòng)作
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、自定義模板函數(shù)
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、靜態(tài)文件處理
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>

五、獲取參數(shù)

Gin框架將處理HTTP請(qǐng)求參數(shù)以及如何響應(yīng)等操作都封裝到了gin.Conetxt結(jié)構(gòu)體,并為gin.Context提供了非常多的方法,因此了解gin.Context的結(jié)構(gòu)定義與方法,對(duì)使用Gin框架編寫Web項(xiàng)目非常重要。

// 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的結(jié)構(gòu)定義來(lái)看,gin.Context封裝了http.Request和http.ResponseWriter。

1、獲取路徑中的參數(shù)(path)

path是指請(qǐng)求的url中域名之后從/開始的部分,如訪問(wèn)web地址:https://localhost:8080/user/jack,/user/jack部分便是path,可以使用gin.Context中提供的方法獲取這部分參數(shù)。

獲取路徑中的參數(shù)有兩種方法:

  • 使用gin.Context的中Param()方法獲取path中的參數(shù)
  • 使用gin.Context中的Params字段獲取path中的參數(shù)
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()

    // 此規(guī)則能夠匹配/user/john這種格式,但不能匹配/user/ 或 /user這種格式
    router.GET("/user/:name", func(c *gin.Context) {
        name := c.Param("name")
        c.String(http.StatusOK, "Hello %s", name)
    })

    // 但是,這個(gè)規(guī)則既能匹配/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請(qǐng)求參數(shù)(query)

query是指url請(qǐng)求地址中的問(wèn)號(hào)后面的部,稱為查詢參數(shù)。如https://localhost:8080/index?name=jack&id=100name=jack&id=100就是查詢參數(shù)。

gin.Context提供了以下幾個(gè)方法,用于獲取Query部分的參數(shù):

  • 獲取單個(gè)參數(shù)
func (c *Context) GetQuery(key string) (string, bool) {}
func (c *Context) Query(key string) string {}
func (c *Context) DefaultQuery(key, defaultValue string) string {}

上面三個(gè)方法用于獲取單個(gè)數(shù)值,GetQuery比Query多返回一個(gè)error類型的參數(shù),實(shí)際上Query方法只是封裝了GetQuery方法,并忽略GetQuery方法返回的錯(cuò)誤而已,而DefaultQuery方法則在沒有獲取相應(yīng)參數(shù)值的返回一個(gè)默認(rèn)值。

  • 獲取數(shù)組
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()
}

請(qǐng)求:http://localhost:8080/index?name=jack&id=100
響應(yīng):Hello, name:jack, id:100
請(qǐng)求:http://localhost:8080/user?id=10&id=20&id=40
響應(yīng):{"ids":["10","20","40"]}
請(qǐng)求:http://localhost:8080/article?articles[tittle]=golang
響應(yīng):{"tittle":"golang"}

3、獲取post請(qǐng)求參數(shù)(body)

一般HTTP的Post請(qǐng)求參數(shù)都是通過(guò)body部分傳給服務(wù)器端的,尤其是數(shù)據(jù)量大或安全性要求較高的數(shù)據(jù),如登錄功能中的賬號(hào)密碼等參數(shù)。

gin.Context提供了以下四個(gè)方法讓我們獲取body中的數(shù)據(jù),不過(guò)要說(shuō)明的是,下面的四個(gè)方法,只能獲取Content-type是application/x-www-form-urlencoded或multipart/form-data時(shí)body中的數(shù)據(jù)。

示例代碼:

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>

六、數(shù)據(jù)綁定

我們直接使用gin.Context提供的方法獲取請(qǐng)求中通過(guò)path、query、body帶上來(lái)的參數(shù),但使用前面的那些方法,并不能處理請(qǐng)求中比較復(fù)雜的數(shù)據(jù)結(jié)構(gòu),比如Content-type為application/json或application/xml時(shí),其所帶上的數(shù)據(jù)會(huì)很復(fù)雜,因此我們需要使用另外一種方法獲取這些數(shù)據(jù),這種方式叫數(shù)據(jù)綁定。

Gin框架將數(shù)據(jù)綁定的操作都封裝在gin/binding這個(gè)包中,下面是gin/binding包定義的常量,說(shuō)明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提交數(shù)據(jù)的處理結(jié)構(gòu)體,并以變量的形式讓其他包可以訪問(wèn),如下:

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

但實(shí)際上,我們并不需要調(diào)用gin/binding包的代碼來(lái)完成數(shù)據(jù)綁定的功能,因?yàn)間in.Context中已經(jīng)在gin.Context的基礎(chǔ)上封裝了許多更加快捷的方法供我們使用,gin提供了兩套綁定方法:

  • Must bind
    Methods方法:Bind, BindJSON, BindXML, BindQuery, BindYAML
    Behavior行為:這些方法底層使用 MustBindWith,如果存在綁定錯(cuò)誤,請(qǐng)求將被中止,返回http狀態(tài)為400的響應(yīng)給客戶端。

  • Should bind
    Methods方法:ShouldBind, ShouldBindJSON, ShouldBindXML, ShouldBindQuery, ShouldBindYAML
    Behavior行為:這些方法底層使用 ShouldBindWith,如果存在綁定錯(cuò)誤,則返回錯(cuò)誤,開發(fā)人員可以正確處理請(qǐng)求和錯(cuò)誤。

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

當(dāng)我們?cè)贖TTP請(qǐng)求中Body設(shè)置不同數(shù)據(jù)格式,需要設(shè)置相應(yīng)頭部Content-Type的值,比較常用的為json、xml、yaml,gin.Context提供下面三個(gè)方法綁定對(duì)應(yīng)Content-type時(shí)body中的數(shù)據(jù)。

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

除了上面三個(gè)方法外,更常用的Bind()方法,Bind()方法會(huì)自動(dòng)根據(jù)Content-Type的值選擇不同的綁定類型。

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

上面幾個(gè)方法都是獲取固定Content-type或自動(dòng)根據(jù)Content-type選擇綁定類型,我們也可以使用下面兩個(gè)方法自行選擇綁定類型。

// 第二個(gè)參數(shù)值是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 結(jié)構(gòu)體
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>

請(qǐng)求:http://localhost:8080/user/jack/123456
響應(yīng):{"username":"jack","password":"123456","bobbys":null}
請(qǐng)求:http://localhost:8080/index?username=jack&password=123456
響應(yīng):{"username":"jack","password":"123456","bobbys":null}
請(qǐng)求:http://localhost:8080/register 輸入username和password、hobbys,提交
響應(yīng):{"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")
    })
    // 內(nèi)部路由重定向
    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) {...})

還有一個(gè)可以匹配所有請(qǐng)求方法的Any方法如下:

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

為沒有配置處理函數(shù)的路由添加處理程序,默認(rèn)情況下它返回404代碼,下面的代碼為沒有匹配到路由的請(qǐng)求都返回views/404.html頁(yè)面。

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

我們可以將擁有共同URL前綴的路由劃分為一個(gè)路由組。習(xí)慣性一對(duì){}包裹同組的路由,這只是為了看著清晰,你用不用{}包裹功能上沒什么區(qū)別。

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框架允許開發(fā)者在處理請(qǐng)求的過(guò)程中,加入用戶自己的鉤子(Hook)函數(shù)。這個(gè)鉤子函數(shù)就叫中間件,中間件適合處理一些公共的業(yè)務(wù)邏輯,比如登錄認(rèn)證、權(quán)限校驗(yàn)、數(shù)據(jù)分頁(yè)、記錄日志、耗時(shí)統(tǒng)計(jì)等。簡(jiǎn)單來(lái)說(shuō),Gin中間件的作用有兩個(gè):

  • Web請(qǐng)求到到達(dá)我們定義的HTTP請(qǐng)求處理方法之前,攔截請(qǐng)求并進(jìn)行相應(yīng)處理(比如:權(quán)限驗(yàn)證,數(shù)據(jù)過(guò)濾等),這個(gè)可以類比為 前置攔截器 或 前置過(guò)濾器 ,

  • 在我們處理完成請(qǐng)求并響應(yīng)客戶端時(shí),攔截響應(yīng)并進(jìn)行相應(yīng)的處理(比如:添加統(tǒng)一響應(yīng)部頭或數(shù)據(jù)格式等),這可以類型為 后置攔截器 或 后置過(guò)濾器 。

1、內(nèi)置中間件

Gin內(nèi)置一些中間件,我們可以直接使用,下面是內(nèi)置中間件列表:

func BasicAuth(accounts Accounts) HandlerFunc {}
func BasicAuthForRealm(accounts Accounts, realm string) HandlerFunc {}
func Bind(val interface{}) HandlerFunc {} //攔截請(qǐng)求參數(shù)并進(jìn)行綁定
func ErrorLogger() HandlerFunc {}       //錯(cuò)誤日志處理
func ErrorLoggerT(typ ErrorType) HandlerFunc {} //自定義類型的錯(cuò)誤日志處理
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中的中間件必須是一個(gè)gin.HandlerFunc類型。

// gin
type HandlerFunc func(*Context)

(1)定義一個(gè)gin.HandleFunc類型的函數(shù)作為中間件:

示例代碼:

package main

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

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

// StatCost 是一個(gè)計(jì)算耗時(shí)的中間件
func StatCost(c *gin.Context) {
    // 傳遞數(shù)據(jù)
    c.Set("name", "jack")
    start := time.Now()
    // 調(diào)用該請(qǐng)求的剩余處理程序
    c.Next()
    // 不調(diào)用該請(qǐng)求的剩余處理程序
    // c.Abort()
    // 計(jì)算耗時(shí)
    cost := time.Since(start)
    fmt.Println(cost)
}

func main() {
    router := gin.Default()
    // 為/路由注冊(cè)中間件StatCost
    router.GET("/", StatCost, func(c *gin.Context) {
        // 獲取中間件傳遞的數(shù)據(jù)
        name := c.MustGet("name").(string)
        c.JSON(http.StatusOK, gin.H{
            "name": name,
        })
    })
    router.Run()
}

(2)通過(guò)自定義方法,返回一個(gè)中間件函數(shù),這是Gin框架中更常用的方式:

示例代碼:

//定義一個(gè)返回中間件的方法
func MyMiddleware(){
    //自定義邏輯
    
    //返回中間件
    return func(c *gin.Context){
        //中間件邏輯
    }
}
3、注冊(cè)中間件

在gin框架中,我們可以為每個(gè)路由添加任意數(shù)量的中間件。

  • 全局使用中間件

直拉使用 gin.Engine 結(jié)構(gòu)體的 Use() 方法便可以在所有請(qǐng)求應(yīng)用中間件,這樣做,中間件便會(huì)在全局起作用。

router.Use(gin.Recovery())//在全局使用內(nèi)置中間件
  • 為某個(gè)路由單獨(dú)注冊(cè)

單個(gè)請(qǐng)求路由,也可以應(yīng)用中間件,如下:

router := gin.New()
router.GET("/test",gin.Recovery(),gin.Logger(),func(c *gin.Context){
    c.JSON(200,"test")
})
  • 為路由組注冊(cè)中間件

根據(jù)業(yè)務(wù)不同劃分不同 路由分組(RouterGroup ),不同的路由分組再應(yīng)用不同的中間件,這樣就達(dá)到了不同的請(qǐng)求由不同的中間件進(jìn)行攔截處理。

為路由組注冊(cè)中間件有以下兩種寫法。

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默認(rèn)中間件

gin.Default()默認(rèn)使用了Logger和Recovery中間件,其中:

Logger中間件將日志寫入gin.DefaultWriter,即使配置了GIN_MODE=release。
Recovery中間件會(huì)recover任何panic。如果有panic的話,會(huì)寫入500響應(yīng)碼。
如果不想使用上面兩個(gè)默認(rèn)的中間件,可以使用gin.New()新建一個(gè)沒有任何默認(rèn)中間件的路由。

(2)數(shù)據(jù)傳遞

當(dāng)我們?cè)谥虚g件攔截并預(yù)先處理好數(shù)據(jù)之后,要如何將數(shù)據(jù)傳遞我們定義的處理請(qǐng)求的HTTP方法呢?可以使用 gin.Context 中的 Set() 方法,其定義如下, Set() 通過(guò)一個(gè)key來(lái)存儲(chǔ)作何類型的數(shù)據(jù),方便下一層處理方法獲取。

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

當(dāng)我們?cè)谥虚g件中通過(guò)Set方法設(shè)置一些數(shù)值,在下一層中間件或HTTP請(qǐng)求處理方法中,可以使用下面列出的方法通過(guò)key獲取對(duì)應(yīng)數(shù)據(jù)。

其中,gin.Context的Get方法返回 interface{} ,通過(guò)返回exists可以判斷key是否存在。

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

當(dāng)我們確定通過(guò)Set方法設(shè)置對(duì)應(yīng)數(shù)據(jù)類型的值時(shí),可以使用下面方法獲取應(yīng)數(shù)據(jù)類型的值。

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)攔截請(qǐng)求與后置攔截

  • 攔截請(qǐng)求

中間件的最大作用就是攔截過(guò)濾請(qǐng)求,比如我們有些請(qǐng)求需要用戶登錄或者需要特定權(quán)限才能訪問(wèn),這時(shí)候便可以中間件中做過(guò)濾攔截,當(dāng)用戶請(qǐng)求不合法時(shí),可以使用下面列出的 gin.Context 的幾個(gè)方法中斷用戶請(qǐng)求:

下面三個(gè)方法中斷請(qǐng)求后,直接返回200,但響應(yīng)的body中不會(huì)有數(shù)據(jù)。

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

使用AbortWithStatusJSON()方法,中斷用戶請(qǐng)求后,則可以返回 json格式的數(shù)據(jù).

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

前面我們講的都是到達(dá)我們定義的HTTP處理方法前進(jìn)行攔截,其實(shí),如果在中間件中調(diào)用 gin.Context 的 Next() 方法,則可以請(qǐng)求到達(dá)并完成業(yè)務(wù)處理后,再經(jīng)過(guò)中間件后置攔截處理, Next() 方法定義如下:

func (c *Context) Next()

在中間件調(diào)用 Next() 方法, Next() 方法之前的代碼會(huì)在到達(dá)請(qǐng)求方法前執(zhí)行, Next() 方法之后的代碼則在請(qǐng)求方法處理后執(zhí)行:

func MyMiddleware(c *gin.Context){
    //請(qǐng)求前
    c.Next()
    //請(qǐng)求后
}

(4)gin中間件中使用goroutine

當(dāng)在中間件或handler中啟動(dòng)新的goroutine時(shí),不能使用原始的上下文(c *gin.Context),必須使用其只讀副本(c.Copy())。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容