1. 程式人生 > 實用技巧 >【go語言學習】web開發框架gin

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