Gin
官方文檔:Gin Web Framework (gin-gonic.com)
倉庫地址:gin-gonic/gin: Gin is a HTTP web framework written in Go (Golang)
官方示例:gin-gonic/examples: A repository to host examples and tutorials for Gin. (github.com)
介紹
Gin 是一個用 Go (Golang) 編寫的 Web 框架。 它具有類似 martini 的 API,性能要好得多,多虧了 httprouter,速度提高了 40 倍。 如果您需要性能和良好的生產力,您一定會喜歡 Gin。Gin 相比於 Iris 和 Beego 而言,更傾向於輕量化的框架,只負責 Web 部分,追求極致的路由性能,功能或許沒那麼全,勝在輕量易拓展,這也是它的優點。因此,在所有的 Web 框架中,Gin 是最容易上手和學習的。
特性
- 快速:基於 Radix 樹的路由,小內存佔用。沒有反射。可預測的 API 性能。
- 支持中間件:傳入的 HTTP 請求可以由一系列中間件和最終操作來處理。 例如:Logger,Authorization,GZIP,最終操作 DB。
- Crash 處理:Gin 可以 catch 一個發生在 HTTP 請求中的 panic 並 recover 它。這樣,你的服務器將始終可用。
- JSON 驗證:Gin 可以解析並驗證請求的 JSON,例如檢查所需值的存在。
- 路由組:更好地組織路由。是否需要授權,不同的 API 版本…… 此外,這些組可以無限制地嵌套而不會降低性能。
- 錯誤管理:Gin 提供了一種方便的方法來收集 HTTP 請求期間發生的所有錯誤。最終,中間件可以將它們寫入日志文件,數據庫並通過網絡發送。
- 內置渲染:Gin 為 JSON,XML 和 HTML 渲染提供了易於使用的 API。
- 可擴展性:新建一個中間件非常簡單
安裝
截止目前2022/11/22,gin 支持的 go 最低版本為1.16,建議使用go mod來管理項目依賴。
go get -u github.com/gin-gonic/gin導入
import "github.com/gin-gonic/gin"快速開始
package main
import (
"github.com/gin-gonic/gin"
"net/http"
)
func main() {
engine := gin.Default() //創建gin引擎
engine.GET("/ping", func(context *gin.Context) {
context.JSON(http.StatusOK, gin.H{
"message": "pong",
})
})
engine.Run() //開啟服務器,默認監聽localhost:8080
}請求 URL
GET localhost:8080/ping返回
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Tue, 22 Nov 2022 08:47:11 GMT
Content-Length: 18
{
"message": "pong"
}
Response file saved.
> 2022-11-22T164711.200.json文檔
其實Gin官方文檔裡面並沒有多少教程,大多數只是一些介紹和基本使用和一些例子,但是gin-gonic/ 組織下,有一個gin-gonic/examples倉庫,這是一個由社區共同維護的gin示例倉庫。都是全英文,更新時間並不是特別頻繁,筆者也是從這裡慢慢學習的gin框架。
示例倉庫地址:gin-gonic/examples: A repository to host examples and tutorials for Gin. (github.com)
TIP
開始之前建議可以閱讀一下HttpRouter:HttpRouter
參數解析
gin中的參數解析總共支持三種方式:路由參數,URL參數,表單參數,下面逐一講解並結合代碼示例,比較簡單易懂。
路由參數
路由參數其實是封裝了HttpRouter的參數解析功能,使用方法基本上與HttpRouter一致。
package main
import (
"github.com/gin-gonic/gin"
"log"
"net/http"
)
func main() {
e := gin.Default()
e.GET("/findUser/:username/:userid", FindUser)
e.GET("/downloadFile/*filepath", UserPage)
log.Fatalln(e.Run(":8080"))
}
// 命名參數示例
func FindUser(c *gin.Context) {
username := c.Param("username")
userid := c.Param("userid")
c.String(http.StatusOK, "username is %s\n userid is %s", username, userid)
}
// 路徑參數示例
func UserPage(c *gin.Context) {
filepath := c.Param("filepath")
c.String(http.StatusOK, "filepath is %s", filepath)
}示例一
curl --location --request GET '127.0.0.1:8080/findUser/jack/001'username is jack
userid is 001示例二
curl --location --request GET '127.0.0.1:8080/downloadFile/img/fruit.png'filepath is /img/fruit.pngURL 參數
傳統的 URL 參數,格式就是/url?key=val&key1=val1&key2=val2。
package main
import (
"github.com/gin-gonic/gin"
"log"
"net/http"
)
func main() {
e := gin.Default()
e.GET("/findUser", FindUser)
log.Fatalln(e.Run(":8080"))
}
func FindUser(c *gin.Context) {
username := c.DefaultQuery("username", "defaultUser")
userid := c.Query("userid")
c.String(http.StatusOK, "username is %s\nuserid is %s", username, userid)
}示例一
curl --location --request GET '127.0.0.1:8080/findUser?username=jack&userid=001'username is jack
userid is 001示例二
curl --location --request GET '127.0.0.1:8080/findUser'username is defaultUser
userid is表單參數
表單的內容類型一般有application/json,application/x-www-form-urlencoded,application/xml,multipart/form-data。
package main
import (
"github.com/gin-gonic/gin"
"net/http"
)
func main() {
e := gin.Default()
e.POST("/register", RegisterUser)
e.POST("/update", UpdateUser)
e.Run(":8080")
}
func RegisterUser(c *gin.Context) {
username := c.PostForm("username")
password := c.PostForm("password")
c.String(http.StatusOK, "successfully registered,your username is [%s],password is [%s]", username, password)
}
func UpdateUser(c *gin.Context) {
var form map[string]string
c.ShouldBind(&form)
c.String(http.StatusOK, "successfully update,your username is [%s],password is [%s]", form["username"], form["password"])
}示例一:使用form-data
curl --location --request POST '127.0.0.1:8080/register' \
--form 'username="jack"' \
--form 'password="123456"'successfully registered,your username is [jack],password is [123456]PostForm方法默認解析application/x-www-form-urlencoded和multipart/form-data類型的表單。
示例二:使用json
curl --location --request POST '127.0.0.1:8080/update' \
--header 'Content-Type: application/json' \
--data-raw '{
"username":"username",
"password":"123456"
}'successfully update,your username is [username],password is [123456]數據解析
在大多數情況下,我們都會使用結構體來承載數據,而不是直接解析參數。在gin中,用於數據綁定的方法主要是Bind()和ShouldBind(),兩者的區別在於前者內部也是直接調用的ShouldBind(),當然返回err時,會直接進行 400 響應,後者則不會。如果想要更加靈活的進行錯誤處理,建議選擇後者。這兩個函數會自動根據請求的content-type來進行推斷用什麼方式解析。
func (c *Context) MustBindWith(obj any, b binding.Binding) error {
// 調用了ShouldBindWith()
if err := c.ShouldBindWith(obj, b); err != nil {
c.AbortWithError(http.StatusBadRequest, err).SetType(ErrorTypeBind) // 直接響應400 badrequest
return err
}
return nil
}如果想要自行選擇可以使用BindWith()和ShouldBindWith(),例如
c.MustBindWith(obj, binding.JSON) //json
c.MustBindWith(obj, binding.XML) //xmlgin 支持的綁定類型有如下幾種實現:
var (
JSON = jsonBinding{}
XML = xmlBinding{}
Form = formBinding{}
Query = queryBinding{}
FormPost = formPostBinding{}
FormMultipart = formMultipartBinding{}
ProtoBuf = protobufBinding{}
MsgPack = msgpackBinding{}
YAML = yamlBinding{}
Uri = uriBinding{}
Header = headerBinding{}
TOML = tomlBinding{}
)示例
package main
import (
"fmt"
"github.com/gin-gonic/gin"
"net/http"
)
type LoginUser struct {
Username string `binding:"required" json:"username" form:"username" uri:"username"`
Password string `binding:"required" json:"password" form:"password" uri:"password"`
}
func main() {
e := gin.Default()
e.POST("/loginWithJSON", Login)
e.POST("/loginWithForm", Login)
e.GET("/loginWithQuery/:username/:password", Login)
e.Run(":8080")
}
func Login(c *gin.Context) {
var login LoginUser
// 使用ShouldBind來讓gin自動推斷
if c.ShouldBind(&login) == nil && login.Password != "" && login.Username != "" {
c.String(http.StatusOK, "login successfully !")
} else {
c.String(http.StatusBadRequest, "login failed !")
}
fmt.Println(login)
}Json 數據綁定
curl --location --request POST '127.0.0.1:8080/loginWithJSON' \
--header 'Content-Type: application/json' \
--data-raw '{
"username":"root",
"password":"root"
}'login successfully !表單數據綁定
curl --location --request POST '127.0.0.1:8080/loginWithForm' \
--form 'username="root"' \
--form 'password="root"'login successfully !URL 數據綁定
curl --location --request GET '127.0.0.1:8080/loginWithQuery/root/root'login failed !到了這裡就會發生錯誤了,因為這裡輸出的content-type是空字符串,無法推斷到底是要如何進行數據解析。所以當使用 URL 參數時,我們應該手動指定解析方式,例如:
if err := c.ShouldBindUri(&login); err == nil && login.Password != "" && login.Username != "" {
c.String(http.StatusOK, "login successfully !")
} else {
fmt.Println(err)
c.String(http.StatusBadRequest, "login failed !")
}多次綁定
一般方法都是通過調用 c.Request.Body 方法綁定數據,但不能多次調用這個方法,例如c.ShouldBind,不可重用,如果想要多次綁定的話,可以使用
c.ShouldBindBodyWith。
func SomeHandler(c *gin.Context) {
objA := formA{}
objB := formB{}
// 讀取 c.Request.Body 並將結果存入上下文。
if errA := c.ShouldBindBodyWith(&objA, binding.JSON); errA == nil {
c.String(http.StatusOK, `the body should be formA`)
// 這時, 復用存儲在上下文中的 body。
}
if errB := c.ShouldBindBodyWith(&objB, binding.JSON); errB == nil {
c.String(http.StatusOK, `the body should be formB JSON`)
// 可以接受其他格式
}
if errB2 := c.ShouldBindBodyWith(&objB, binding.XML); errB2 == nil {
c.String(http.StatusOK, `the body should be formB XML`)
}
}TIP
c.ShouldBindBodyWith 會在綁定之前將 body 存儲到上下文中。 這會對性能造成輕微影響,如果調用一次就能完成綁定的話,那就不要用這個方法。只有某些格式需要此功能,如 JSON, XML, MsgPack, ProtoBuf。 對於其他格式, 如 Query, Form, FormPost, FormMultipart 可以多次調用c.ShouldBind() 而不會造成任何性能損失 。
數據校驗
gin內置的校驗工具其實是github.com/go-playground/validator/v10,使用方法也幾乎沒有什麼差別,Validator
簡單示例
type LoginUser struct {
Username string `binding:"required" json:"username" form:"username" uri:"username"`
Password string `binding:"required" json:"password" form:"password" uri:"password"`
}
func main() {
e := gin.Default()
e.POST("/register", Register)
log.Fatalln(e.Run(":8080"))
}
func Register(ctx *gin.Context) {
newUser := &LoginUser{}
if err := ctx.ShouldBind(newUser); err == nil {
ctx.String(http.StatusOK, "user%+v", *newUser)
} else {
ctx.String(http.StatusBadRequest, "invalid user,%v", err)
}
}測試
curl --location --request POST 'http://localhost:8080/register' \
--header 'Content-Type: application/json' \
--data-raw '{
"username":"jack1"
}'輸出
invalid user,Key: 'LoginUser.Password' Error:Field validation for 'Password' failed on the 'required' tagTIP
需要注意的一點是,gin 中 validator 的校驗 tag 是binding,而單獨使用validator的的校驗 tag 是validator
數據響應
數據響應是接口處理中最後一步要做的事情,後端將所有數據處理完成後,通過 HTTP 協議返回給調用者,gin 對於數據響應提供了豐富的內置支持,用法簡潔明了,上手十分容易。
簡單示例
func Hello(c *gin.Context) {
// 返回純字符串格式的數據,http.StatusOK代表著200狀態碼,數據為"Hello world !"
c.String(http.StatusOK, "Hello world !")
}HTML 渲染
TIP
文件加載的時候,默認根路徑是項目路徑,也就是go.mod文件所在的路徑,下面例子中的index.html即位於根路徑下的index.html,不過一般情況下這些模板文件都不會放在根路徑,而是會存放在靜態資源文件夾中
func main() {
e := gin.Default()
// 加載HTML文件,也可以使用Engine.LoadHTMLGlob()
e.LoadHTMLFiles("index.html")
e.GET("/", Index)
log.Fatalln(e.Run(":8080"))
}
func Index(c *gin.Context) {
c.HTML(http.StatusOK, "index.html", gin.H{})
}測試
curl --location --request GET 'http://localhost:8080/'返回
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>GinLearn</title>
</head>
<body>
<h1>Hello World!</h1>
<h1>This is a HTML Template Render Example</h1>
</body>
</html>快速響應
前面經常用到context.String()方法來進行數據響應,這是最原始的響應方法,直接返回一個字符串,gin中其實還內置了許多了快速響應的方法例如:
// 使用Render寫入響應頭,並進行數據渲染
func (c *Context) Render(code int, r render.Render)
// 渲染一個HTML模板,name是html路徑,obj是內容
func (c *Context) HTML(code int, name string, obj any)
// 以美化了的縮進JSON字符串進行數據渲染,通常不建議使用這個方法,因為會造成更多的傳輸消耗。
func (c *Context) IndentedJSON(code int, obj any)
// 安全的JSON,可以防止JSON劫持,詳情了解:https://www.cnblogs.com/xusion/articles/3107788.html
func (c *Context) SecureJSON(code int, obj any)
// JSONP方式進行渲染
func (c *Context) JSONP(code int, obj any)
// JSON方式進行渲染
func (c *Context) JSON(code int, obj any)
// JSON方式進行渲染,會將unicode碼轉換為ASCII碼
func (c *Context) AsciiJSON(code int, obj any)
// JSON方式進行渲染,不會對HTML特殊字符串進行轉義
func (c *Context) PureJSON(code int, obj any)
// XML方式進行渲染
func (c *Context) XML(code int, obj any)
// YML方式進行渲染
func (c *Context) YAML(code int, obj any)
// TOML方式進行渲染
func (c *Context) TOML(code int, obj interface{})
// ProtoBuf方式進行渲染
func (c *Context) ProtoBuf(code int, obj any)
// String方式進行渲染
func (c *Context) String(code int, format string, values ...any)
// 重定向到特定的位置
func (c *Context) Redirect(code int, location string)
// 將data寫入響應流中
func (c *Context) Data(code int, contentType string, data []byte)
// 通過reader讀取流並寫入響應流中
func (c *Context) DataFromReader(code int, contentLength int64, contentType string, reader io.Reader, extraHeaders map[string]string)
// 高效的將文件寫入響應流
func (c *Context) File(filepath string)
// 以一種高效的方式將fs中的文件流寫入響應流
func (c *Context) FileFromFS(filepath string, fs http.FileSystem)
// 以一種高效的方式將fs中的文件流寫入響應流,並且在客戶端會以指定的文件名進行下載
func (c *Context) FileAttachment(filepath, filename string)
// 將服務端推送流寫入響應流中
func (c *Context) SSEvent(name string, message any)
// 發送一個流響應並返回一個布爾值,以此來判斷客戶端是否在流中間斷開
func (c *Context) Stream(step func(w io.Writer) bool) bool對於大多數應用而言,用的最多的還是context.JSON,其他的相對而言要少一些,這裡就不舉例子演示了,因為都比較簡單易懂,差不多都是直接調用的事情。
異步處理
在 gin 中,異步處理需要結合 goroutine 使用,使用起來十分簡單。
// copy返回一個當前Context的副本以便在當前Context作用范圍外安全的使用,可以用於傳遞給一個goroutine
func (c *Context) Copy() *Contextfunc main() {
e := gin.Default()
e.GET("/hello", Hello)
log.Fatalln(e.Run(":8080"))
}
func Hello(c *gin.Context) {
ctx := c.Copy()
go func() {
// 子協程應該使用Context的副本,不應該使用原始Context
log.Println("異步處理函數: ", ctx.HandlerNames())
}()
log.Println("接口處理函數: ", c.HandlerNames())
c.String(http.StatusOK, "hello")
}測試
curl --location --request GET 'http://localhost:8080/hello'輸出
2022/12/21 13:33:47 異步處理函數: []
2022/12/21 13:33:47 接口處理函數: [github.com/gin-gonic/gin.LoggerWithConfig.func1 github.com/gin-gonic/gin.CustomRecoveryWithWriter.func1 main.Hello]
[GIN] 2022/12/21 - 13:33:47 | 200 | 11.1927ms | ::1 | GET "/hello"可以看到兩者輸出不同,副本在復制時,為了安全考慮,刪掉了許多元素的值。
文件傳輸
文件傳輸是 Web 應用的一個不可或缺的功能,gin 對於此的支持也是封裝的十分簡單,但其實本質上和用原生的net/http的流程都差不多。流程都是從請求體中讀取文件流,然後再保存到本地。
單文件上傳
func main() {
e := gin.Default()
e.POST("/upload", uploadFile)
log.Fatalln(e.Run(":8080"))
}
func uploadFile(ctx *gin.Context) {
// 獲取文件
file, err := ctx.FormFile("file")
if err != nil {
ctx.String(http.StatusBadRequest, "%+v", err)
return
}
// 保存在本地
err = ctx.SaveUploadedFile(file, "./"+file.Filename)
if err != nil {
ctx.String(http.StatusBadRequest, "%+v", err)
return
}
// 返回結果
ctx.String(http.StatusOK, "upload %s size:%d byte successfully!", file.Filename, file.Size)
}測試
curl --location --request POST 'http://localhost:8080/upload' \
--form 'file=@"/C:/Users/user/Pictures/Camera Roll/a.jpg"'結果
upload a.jpg size:1424 byte successfully!TIP
一般情況下,上傳文件的Method都會指定用POST,一些公司可能會傾向於使用PUT,前者是簡單 HTTP 請求,後者是復雜 HTTP 請求,具體區別不作贅述,如果使用後者的話,尤其是前後端分離的項目時,需要進行相應的跨域處理,而 Gin 默認的配置是不支持跨域的跨域配置。
多文件上傳
func main() {
e := gin.Default()
e.POST("/upload", uploadFile)
e.POST("/uploadFiles", uploadFiles)
log.Fatalln(e.Run(":8080"))
}
func uploadFiles(ctx *gin.Context) {
// 獲取gin解析好的multipart表單
form, _ := ctx.MultipartForm()
// 根據鍵值取得對應的文件列表
files := form.File["files"]
// 遍歷文件列表,保存到本地
for _, file := range files {
err := ctx.SaveUploadedFile(file, "./"+file.Filename)
if err != nil {
ctx.String(http.StatusBadRequest, "upload failed")
return
}
}
// 返回結果
ctx.String(http.StatusOK, "upload %d files successfully!", len(files))
}測試
curl --location --request POST 'http://localhost:8080/uploadFiles' \
--form 'files=@"/C:/Users/Stranger/Pictures/Camera Roll/a.jpg"' \
--form 'files=@"/C:/Users/Stranger/Pictures/Camera Roll/123.jpg"' \
--form 'files=@"/C:/Users/Stranger/Pictures/Camera Roll/girl.jpg"'輸出
upload 3 files successfully!文件下載
關於文件下載的部分 Gin 對於原有標准庫的 API 再一次封裝,使得文件下載異常簡單。
func main() {
e := gin.Default()
e.POST("/upload", uploadFile)
e.POST("/uploadFiles", uploadFiles)
e.GET("/download/:filename", download)
log.Fatalln(e.Run(":8080"))
}
func download(ctx *gin.Context) {
// 獲取文件名
filename := ctx.Param("filename")
// 返回對應文件
ctx.FileAttachment(filename, filename)
}測試
curl --location --request GET 'http://localhost:8080/download/a.jpg'結果
Content-Disposition: attachment; filename="a.jpg"
Date: Wed, 21 Dec 2022 08:04:17 GMT
Last-Modified: Wed, 21 Dec 2022 07:50:44 GMT是不是覺得簡單過頭了,不妨不用框架的方法,自行編寫一遍過程
func download(ctx *gin.Context) {
// 獲取參數
filename := ctx.Param("filename")
// 請求響應對象和請求對象
response, request := ctx.Writer, ctx.Request
// 寫入響應頭
// response.Header().Set("Content-Type", "application/octet-stream") 以二進制流傳輸文件
response.Header().Set("Content-Disposition", `attachment; filename*=UTF-8''`+url.QueryEscape(filename)) // 對文件名進行安全轉義
response.Header().Set("Content-Transfer-Encoding", "binary") // 傳輸編碼
http.ServeFile(response, request, filename)
}其實net/http也已經封裝的足夠好了
TIP
可以通過Engine.MaxMultipartMemory來設置文件傳輸的最大內存,默認為32 << 20 // 32 MB
路由管理
路由管理是一個系統中非常重要的部分,需要確保每一個請求都能被正確的映射到對應的函數上。
路由組
創建一個路由組是將接口分類,不同類別的接口對應不同的功能,也更易於管理。
func Hello(c *gin.Context) {
}
func Login(c *gin.Context) {
}
func Update(c *gin.Context) {
}
func Delete(c *gin.Context) {
}假設我們有以上四個接口,暫時不管其內部實現,Hello,Login是一組,Update,Delete是一組。
func (group *RouterGroup) Group(relativePath string, handlers ...HandlerFunc) *RouterGroup在創建分組的時候,我們也可以給分組的根路由注冊處理器,不過大多數時候並不會這麼做。
func main() {
e := gin.Default()
v1 := e.Group("v1")
{
v1.GET("/hello", Hello)
v1.GET("/login", Login)
}
v2 := e.Group("v2")
{
v2.POST("/update", Update)
v2.DELETE("/delete", Delete)
}
}我們將其分成了v1,v2兩個分組,其中的花括號{}僅僅只是為了規范,表名花括號內注冊的處理器是屬於同一個路由分組,在功能上沒有任何作用。同樣的,gin 也支持嵌套分組,方法與上例一致,這裡就不再演示。
404 路由
gin 中的Engine結構體提供了一個方法NoRoute,來設置當訪問的 URL 不存在時如何處理,開發者可以將邏輯寫入此方法中,以便路由未找到時自動調用,默認會返回 404 狀態碼
func (engine *Engine) NoRoute(handlers ...HandlerFunc)我們拿上個例子舉例
func main() {
e := gin.Default()
v1 := e.Group("v1")
{
v1.GET("/hello", Hello)
v1.GET("/login", Login)
}
v2 := e.Group("v2")
{
v2.POST("/update", Update)
v2.DELETE("/delete", Delete)
}
// 注冊處理器
e.NoRoute(func(context *gin.Context) { // 這裡只是演示,不要在生產環境中直接返回HTML代碼
context.String(http.StatusNotFound, "<h1>404 Page Not Found</h1>")
})
log.Fatalln(e.Run(":8080"))
}隨便發一個請求
curl --location --request GET 'http://localhost:8080/'<h1>404 Page Not Found</h1>405 路由
Http 狀態碼中,405 代表著當前請求的方法類型是不允許的,gin 中提供了如下方法
func (engine *Engine) NoMethod(handlers ...HandlerFunc)來注冊一個處理器,以便在發生時自動調用,前提是設置Engine.HandleMethodNotAllowed = true。
func main() {
e := gin.Default()
// 需要將其設置為true
e.HandleMethodNotAllowed = true
v1 := e.Group("/v1")
{
v1.GET("/hello", Hello)
v1.GET("/login", Login)
}
v2 := e.Group("/v2")
{
v2.POST("/update", Update)
v2.DELETE("/delete", Delete)
}
e.NoRoute(func(context *gin.Context) {
context.String(http.StatusNotFound, "<h1>404 Page Not Found</h1>")
})
// 注冊處理器
e.NoMethod(func(context *gin.Context) {
context.String(http.StatusMethodNotAllowed, "method not allowed")
})
log.Fatalln(e.Run(":8080"))
}配置好後,gin 默認的 header 是不支持OPTION請求的,測試一下
curl --location --request OPTIONS 'http://localhost:8080/v2/delete'method not allowed至此配置成功
重定向
gin 中的重定向十分簡單,調用gin.Context.Redirect()方法即可。
func main() {
e := gin.Default()
e.GET("/", Index)
e.GET("/hello", Hello)
log.Fatalln(e.Run(":8080"))
}
func Index(c *gin.Context) {
c.Redirect(http.StatusMovedPermanently, "/hello")
}
func Hello(c *gin.Context) {
c.String(http.StatusOK, "hello")
}測試
curl --location --request GET 'http://localhost:8080/'輸出
hello中間件
gin 十分輕便靈活,拓展性非常高,對於中間件的支持也非常友好。在 Gin 中,所有的接口請求都要經過中間件,通過中間件,開發者可以自定義實現很多功能和邏輯,gin 雖然本身自帶的功能很少,但是由第三方社區開發的 gin 拓展中間件十分豐富。
中間件本質上其實還是一個接口處理器
// HandlerFunc defines the handler used by gin middleware as return value.
type HandlerFunc func(*Context)從某種意義上來說,每一個請求對應的處理器也是中間件,只不過是作用范圍非常小的局部中間件。
func Default() *Engine {
debugPrintWARNINGDefault()
engine := New()
engine.Use(Logger(), Recovery())
return engine
}查看 gin 的源代碼,Default函數中,返回的默認Engine就使用兩個默認中間件Logger(),Recovery(),如果不想使用默認的中間件也可以使用gin.New()來代替。
全局中間件
全局中間件即作用范圍為全局,整個系統所有的請求都會經過此中間件。
func GlobalMiddleware() gin.HandlerFunc {
return func(ctx *gin.Context) {
fmt.Println("全局中間件被執行...")
}
}先創建一個閉包函數來創建中間件,再通過Engine.Use()來注冊全局中間件。
func main() {
e := gin.Default()
// 注冊全局中間件
e.Use(GlobalMiddleware())
v1 := e.Group("/v1")
{
v1.GET("/hello", Hello)
v1.GET("/login", Login)
}
v2 := e.Group("/v2")
{
v2.POST("/update", Update)
v2.DELETE("/delete", Delete)
}
log.Fatalln(e.Run(":8080"))
}測試
curl --location --request GET 'http://localhost:8080/v1/hello'輸出
[GIN-debug] Listening and serving HTTP on :8080
全局中間件被執行...
[GIN] 2022/12/21 - 11:57:52 | 200 | 538.9µs | ::1 | GET "/v1/hello"局部中間件
局部中間件即作用范圍為局部,系統中局部的請求會經過此中間件。局部中間件可以注冊到單個路由上,不過更多時候是注冊到路由組上。
func main() {
e := gin.Default()
// 注冊全局中間件
e.Use(GlobalMiddleware())
// 注冊路由組局部中間件
v1 := e.Group("/v1", LocalMiddleware())
{
v1.GET("/hello", Hello)
v1.GET("/login", Login)
}
v2 := e.Group("/v2")
{
// 注冊單個路由局部中間件
v2.POST("/update", LocalMiddleware(), Update)
v2.DELETE("/delete", Delete)
}
log.Fatalln(e.Run(":8080"))
}測試
curl --location --request POST 'http://localhost:8080/v2/update'輸出
全局中間件被執行...
局部中間件被執行
[GIN] 2022/12/21 - 12:05:03 | 200 | 999.9µs | ::1 | POST "/v2/update"中間件原理
Gin 中間的使用和自定義非常容易,其內部的原理也比較簡單,為了後續的學習,需要簡單的了解下內部原理。Gin 中的中間件其實用到了責任鏈模式,Context中維護著一個HandlersChain,本質上是一個[]HandlerFunc,和一個index,其數據類型為int8。在Engine.handlerHTTPRequest(c *Context)方法中,有一段代碼表明了調用過程:gin 在路由樹中找到了對應的路由後,便調用了Next()方法。
if value.handlers != nil {
// 將調用鏈賦值給Context
c.handlers = value.handlers
c.fullPath = value.fullPath
// 調用中間件
c.Next()
c.writermem.WriteHeaderNow()
return
}Next()的調用才是關鍵,Next()會遍歷路由的handlers中的HandlerFunc 並執行,此時可以看到index的作用就是記錄中間件的調用位置。其中,給對應路由注冊的接口函數也在handlers內,這也就是為什麼前面會說接口也是一個中間件。
func (c *Context) Next() {
// 一進來就+1是為了避免陷入遞歸死循環,默認值是-1
c.index++
for c.index < int8(len(c.handlers)) {
// 執行HandlerFunc
c.handlers[c.index](c)
// 執行完畢,index+1
c.index++
}
}修改一下Hello()的邏輯,來驗證是否果真如此
func Hello(c *gin.Context) {
fmt.Println(c.HandlerNames())
}輸出結果為
[github.com/gin-gonic/gin.LoggerWithConfig.func1 github.com/gin-gonic/gin.CustomRecoveryWithWriter.func1 main.GlobalMiddleware.func1 main.LocalMiddleware.func1 main.Hello]可以看到中間件調用鏈的順序為:Logger -> Recovery -> GlobalMiddleware -> LocalMiddleWare -> Hello,調用鏈的最後一個元素才是真正要執行的接口函數,前面的都是中間件。
TIP
在注冊局部路由時,有如下一個斷言
finalSize := len(group.Handlers) + len(handlers) //中間件總數
assert1(finalSize < int(abortIndex), "too many handlers")其中abortIndex int8 = math.MaxInt8 >> 1值為 63,即使用系統時路由注冊數量不要超過 63 個。
計時器中間件
在知曉了上述的中間件原理後,就可以編寫一個簡單的請求時間統計中間件。
func TimeMiddleware() gin.HandlerFunc {
return func(context *gin.Context) {
// 記錄開始時間
start := time.Now()
// 執行後續調用鏈
context.Next()
// 計算時間間隔
duration := time.Since(start)
// 輸出納秒,以便觀測結果
fmt.Println("請求用時: ", duration.Nanoseconds())
}
}
func main() {
e := gin.Default()
// 注冊全局中間件,計時中間件
e.Use(GlobalMiddleware(), TimeMiddleware())
// 注冊路由組局部中間件
v1 := e.Group("/v1", LocalMiddleware())
{
v1.GET("/hello", Hello)
v1.GET("/login", Login)
}
v2 := e.Group("/v2")
{
// 注冊單個路由局部中間件
v2.POST("/update", LocalMiddleware(), Update)
v2.DELETE("/delete", Delete)
}
log.Fatalln(e.Run(":8080"))
}測試
curl --location --request GET 'http://localhost:8080/v1/hello'輸出
請求用時: 517600一個簡單的計時器中間件就已經編寫完畢了,後續可以憑借自己的摸索編寫一些功能更實用的中間件。
服務配置
光是使用默認的配置是遠遠不夠的,大多數情況下都需求修改很多的服務配置才能達到需求。
Http 配置
可以通過net/http創建 Server 來配置,Gin 本身也支持像原生 API 一樣使用 Gin。
func main() {
router := gin.Default()
server := &http.Server{
Addr: ":8080",
Handler: router,
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
MaxHeaderBytes: 1 << 20,
}
log.Fatal(server.ListenAndServe())
}靜態資源配置
靜態資源在以往基本上是服務端不可或缺的一部分,盡管在現在使用佔比正在逐漸減少,但仍舊有大量的系統還是使用單體架構的情況。
Gin 提供了三個方法來加載靜態資源
// 加載某一靜態文件夾
func (group *RouterGroup) Static(relativePath, root string) IRoutes
// 加載某一個fs
func (group *RouterGroup) StaticFS(relativePath string, fs http.FileSystem) IRoutes
// 加載某一個靜態文件
func (group *RouterGroup) StaticFile(relativePath, filepath string) IRoutesTIP
relativePath 是映射到網頁 URL 上的相對路徑,root 是文件在項目中的實際路徑
假設項目的目錄如下
root
|
|-- static
| |
| |-- a.jpg
| |
| |-- favicon.ico
|
|-- view
|
|-- htmlfunc main() {
router := gin.Default()
// 加載靜態文件目錄
router.Static("/static", "./static")
// 加載靜態文件目錄
router.StaticFS("/view", http.Dir("view"))
// 加載靜態文件
router.StaticFile("/favicon", "./static/favicon.ico")
router.Run(":8080")
}跨域配置
Gin 本身是沒有對於跨域配置做出任何處理,需要自行編寫中間件來進行實現相應的需求,其實難度也不大,稍微熟悉 HTTP 協議的人一般都能寫出來,邏輯基本上都是那一套。
func CorsMiddle() gin.HandlerFunc {
return func(c *gin.Context) {
method := c.Request.Method
origin := c.Request.Header.Get("Origin")
if origin != "" {
// 生產環境中的服務端通常都不會填 *,應當填寫指定域名
c.Header("Access-Control-Allow-Origin", origin)
// 允許使用的HTTP METHOD
c.Header("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE, UPDATE")
// 允許使用的請求頭
c.Header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, Authorization")
// 允許客戶端訪問的響應頭
c.Header("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Cache-Control, Content-Language, Content-Type")
// 是否需要攜帶認證信息 Credentials 可以是 cookies、authorization headers 或 TLS client certificates
// 設置為true時,Access-Control-Allow-Origin不能為 *
c.Header("Access-Control-Allow-Credentials", "true")
}
// 放行OPTION請求,但不執行後續方法
if method == "OPTIONS" {
c.AbortWithStatus(http.StatusNoContent)
}
// 放行
c.Next()
}
}將中間件注冊為全局中間件即可
會話控制
在目前的時代中,流行的三種 Web 會話控制總共有三種,cookie,session,JWT。
Cookie
ookie 中的信息是以鍵值對的形式儲存在瀏覽器中,而且在瀏覽器中可以直接看到數據
優點:
- 結構簡單
- 數據持久
缺點:
大小受限
明文存儲
容易受到 CSRF 攻擊
import (
"fmt"
"github.com/gin-gonic/gin"
)
func main() {
router := gin.Default()
router.GET("/cookie", func(c *gin.Context) {
// 獲取對應的cookie
cookie, err := c.Cookie("gin_cookie")
if err != nil {
cookie = "NotSet"
// 設置cookie 參數:key,val,存在時間,目錄,域名,是否允許他人通過js訪問cookie,僅http
c.SetCookie("gin_cookie", "test", 3600, "/", "localhost", false, true)
}
fmt.Printf("Cookie value: %s \n", cookie)
})
router.Run()
}單純的 cookie 在五六年前用的比較多,不過作者一般很少使用單純的 cookie 來做會話控制,這樣做確實不太安全。
Session
session 存儲在服務器中,然後發送一個 cookie 存儲在瀏覽器中,cookie 中存儲的是 session_id,之後每次請求服務器通過 session_id 可以獲取對應的 session 信息
優點:
- 存儲在服務端,增加安全性,便於管理
缺點:
- 存儲在服務端,增大服務器開銷,降低性能
- 基於 cookie 識別,不安全
- 認證信息在分布式情況下不同步
Session 與 Cookie 是不分家的,每次要用到 Session,默認就是要用到 Cookie 了。Gin 默認是不支持 Session 的,因為 Cookie 是 Http 協議裡面的內容,但 Session 不是,不過有第三方中間件支持,安裝依賴即可,倉庫地址:gin-contrib/sessions: Gin middleware for session management (github.com)
go get github.com/gin-contrib/sessions支持 cookie,Redis,MongoDB,GORM,PostgreSQL
func main() {
r := gin.Default()
// 創建基於Cookie的存儲引擎
store := cookie.NewStore([]byte("secret"))
// 設置Session中間件,mysession即session名稱,也是cookie的名稱
r.Use(sessions.Sessions("mysession", store))
r.GET("/incr", func(c *gin.Context) {
// 初始化session
session := sessions.Default(c)
var count int
// 獲取值
v := session.Get("count")
if v == nil {
count = 0
} else {
count = v.(int)
count++
}
// 設置
session.Set("count", count)
// 保存
session.Save()
c.JSON(200, gin.H{"count": count})
})
r.Run(":8000")
}一般不推薦通過 Cookie 存儲 Sesison,推薦使用 Redis,其他例子還請自行去官方倉庫了解。
JWT
優點:
- 基於 JSON,多語言通用
- 可以存儲非敏感信息
- 佔用很小,便於傳輸
- 服務端無需存儲,利於分布式拓展
缺點:
- Token 刷新問題
- 一旦簽發則無法主動控制
自從前端革命以來,前端程序員不再只是一個「寫頁面的」,前後端分離的趨勢愈演愈烈,JWT 是最適合前後端分離和分布式系統來做會話控制的,具有很大的天然優勢。考慮到 JWT 已經完全脫離 Gin 的內容,且沒有任何中間件支持,因為 JWT 本身就是不局限於任何框架任何語言,在這裡就不作細致的講解,可以前往另一篇文檔:JWT
日志管理
Gin 默認使用的日志中間件采用的是os.Stdout,只有最基本的功能,畢竟 Gin 只專注於 Web 服務,大多數情況下應該使用更加成熟的日志框架,不過這並不在本章的討論范圍內,而且 Gin 的拓展性很高,可以很輕易的整合其他框架,這裡只討論其自帶的日志服務。
控制台顏色
gin.DisableConsoleColor() // 關閉控制台日志顏色除了在開發的時候,大多數時候都不建議開啟此項
日志寫入文件
func main() {
e := gin.Default()
// 關掉控制台顏色
gin.DisableConsoleColor()
// 創建兩個日志文件
log1, _ := os.Create("info1.log")
log2, _ := os.Create("info2.log")
// 同時記錄進兩個日志文件
gin.DefaultWriter = io.MultiWriter(log1, log2)
e.GET("/hello", Hello)
log.Fatalln(e.Run(":8080"))
}gin 自帶的日志支持寫入多個文件,但內容是相同的,使用起來不太方便,並且不會將請求日志寫入文件中。
func main() {
router := gin.New()
// LoggerWithFormatter 中間件會寫入日志到 gin.DefaultWriter
// 默認 gin.DefaultWriter = os.Stdout
router.Use(gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string {
//TODO 寫入對應文件的邏輯
......
// 輸出自定義格式
return fmt.Sprintf("%s - [%s] \"%s %s %s %d %s \"%s\" %s\"\n",
param.ClientIP,
param.TimeStamp.Format(time.RFC1123),
param.Method,
param.Path,
param.Request.Proto,
param.StatusCode,
param.Latency,
param.Request.UserAgent(),
param.ErrorMessage,
)
}))
router.Use(gin.Recovery())
router.GET("/ping", func(c *gin.Context) {
c.String(200, "pong")
})
router.Run(":8080")
}通過自定義中間件,可以實現日志寫入文件中
路由調試日志格式
這裡修改的只是啟動時輸出路由信息的的日志
func main() {
e := gin.Default()
gin.SetMode(gin.DebugMode)
gin.DebugPrintRouteFunc = func(httpMethod, absolutePath, handlerName string, nuHandlers int) {
if gin.IsDebugging() {
log.Printf("路由 %v %v %v %v\n", httpMethod, absolutePath, handlerName, nuHandlers)
}
}
e.GET("/hello", Hello)
log.Fatalln(e.Run(":8080"))
}輸出
2022/12/21 17:19:13 路由 GET /hello main.Hello 3結語:Gin 算是 Go 語言 Web 框架中最易學習的一種,因為 Gin 真正做到了職責最小化,只是單純的負責 Web 服務,其他的認證邏輯,數據緩存等等功能都交給開發者自行完成,相比於那些大而全的框架,輕量簡潔的 Gin 對於初學者而言更適合也更應該去學習,因為 Gin 並沒有強制使用某一種規范,項目該如何構建,采用什麼結構都需要自行斟酌,對於初學者而言更能鍛煉能力。
