前言
Gin
是一個用Go語言編寫的web框架。它是一個類似於martini
但擁有更好性能的API框架, 由於使用了httprouter
,速度提高了近40倍。
如果你是性能和高效的追求者, 你會愛上Gin
。
Go語言里最流行的Web框架,Github上有32K+
star。 基於httprouter開發的Web框架。 中文文檔齊全,簡單易用的輕量級框架。
安裝
D:\learning-gin>set GOPROXY=https://goproxy.cn
-------------------------------------------------------
D:\learning-gin>go get -u github.com/gin-gonic/gin
go: google.golang.org/protobuf upgrade => v1.25.0 go: gopkg.in/yaml.v2 upgrade => v2.4.0 go: github.com/golang/protobuf upgrade => v1.4.3 go: github.com/ugorji/go/codec upgrade => v1.2.1 go: golang.org/x/sys upgrade => v0.0.0-20201211090839-8ad439b19e0f go: github.com/json-iterator/go upgrade => v1.1.10 go: github.com/modern-go/reflect2 upgrade => v1.0.1 go: github.com/go-playground/validator/v10 upgrade => v10.4.1 go: github.com/modern-go/concurrent upgrade => v0.0.0-20180306012644-bacd9c7ef1dd go: golang.org/x/crypto upgrade => v0.0.0-20201208171446-5f87f3452ae9
Gin簡單示例
package main import "github.com/gin-gonic/gin" func index(c *gin.Context) { //返回json類型的數據,h=type H map[string]interface{} c.JSON(200, gin.H{"msg": "您好呀!"}, ) } func main() { //定義1個默認路由(基於httprouter的) router := gin.Default() //增加url router.GET("/index", index) //server段開始linsten運行 router.Run("127.0.0.1:8000") }
request&response Header
vue設置請求頭中的token
this.$http.defaults.headers.common.X-token = 'sidhlmajldhbd-vue'
Gin獲取請求頭中的Token
token := c.Request.Header.Get("X-token")
Gin響應頭設置Token
c.Header("X-token","sidhlmajldhbd-gin")
Gin request
我們可以通過gin的context獲取到客戶端請求攜帶的url參數、form表單、json數據、文件等。
package main import ( "fmt" "github.com/gin-gonic/gin" ) type user struct { Name string `json:"name"` City string `json:"city"` } var person=&user{} //從url獲取參數 func urlData(c *gin.Context) { //name:=c.Query("name") //獲取url參數,獲取不到獲取空字符串 name:=c.DefaultQuery("name","zhanggen") ////獲取url參數,獲取不到獲取默認! city:=c.DefaultQuery("city","bj") person.Name=name person.City=city c.JSON(200,person) } //從form表單中獲取數據 func formData(c *gin.Context) { c.PostForm("name") person.Name=c.DefaultPostForm("name","Martin") person.City=c.DefaultPostForm("city","London") c.JSON(200,person) } //獲取url地址參數 func pathData(c *gin.Context){ person.City=c.Param("city") person.Name=c.Param("name") c.JSON(200,*person) } //獲取json數據 func jsonData(c *gin.Context){ c.Bind(person) fmt.Println("-----------------",*person) c.JSON(200,person) } func main() { r:=gin.Default() //http://127.0.0.1:8001/user?name=zhanggen&city=beijing r.GET("/user",urlData) r.POST("/user",formData) //http://127.0.0.1:8001/user/bj/zhanggen r.GET("/user/:city/:name",pathData) r.POST("/user/json/",jsonData) r.Run(":8001") }
Gin shouldBind
默認情況下,我們需要根據客戶端請求的content-type,在后端使用不同的方式,獲取客戶端請求參數。
獲取個請求參還需要c.Query、c.PostForm、c.Bind、C.Param,這也太麻煩了~
shouldBind可幫助我們根據客戶端request的content-type,自動獲取請求參數,並賦值給后端struct的字段。
package main import ( "fmt" "github.com/gin-gonic/gin" ) //0.contentType對應ShouldBind對應的結構體 type UserInfo struct { Username string `form:"username" json:"username"` Password string `form:"password" json:"password"` } func index(c *gin.Context) { requestMethod := c.Request.Method //1.聲明1個值類型uerinfo類型的變量u var user UserInfo //2.把客戶端request請求的參數和后端的結合體字段進行綁定 err := c.ShouldBind(&user) if err != nil { c.JSON(400, gin.H{"err": err.Error()}) return } //3.可以通過反射的方式,根據客戶端request的contentType自動獲取數據了 if requestMethod == "GET" { fmt.Println(user) c.HTML(200, "index.html", gin.H{}) } if requestMethod == "POST" { fmt.Println(user) c.JSON(200, gin.H{"data": "postOkay"}) } } func main() { router := gin.Default() router.Static("/static", "./static") router.LoadHTMLGlob("templates/*") router.GET("/user", index) router.POST("/user", index) router.Run(":8002") }
Gin response
我們web開發過程中,大型項目會采用MVVM(前后端分離)的架構,小型項目會采用MTV(模板渲染)的架構。
疏通同歸其目的都是完成數據驅動視圖,不同的是數據驅動視圖的地方不一樣。
貌似web開發玩得就是這6個字,數據----》 驅動-----》視圖。空談誤國,怎么才能更好的驅動視圖才是關鍵。
MTV模式(模板渲染):后端使用模板語法也就是字符串替換的方式,在后端直接完成數據和HTML的渲染,直接返回給客戶端。
MVVM(前后端分離架構):后端返回json數據,前端使用axios/ajax的方式獲取到數據,使用vue等前端框架完成數據到HTML的渲染。
1.RESTful API
只要API程序遵循了REST風格,那就可以稱其為RESTful API。
其實核心思想是1個資源對應1個URL,客戶端對這1資源操作時(不同的request.get/post/put/delete方法)對應后端的增/刪/改/查操作。
例如,我們現在要編寫一個管理書籍的系統,我們可以查詢對一本書進行查詢、創建、更新和刪除等操作。
我們在編寫程序的時候就要設計客戶端瀏覽器與我們Web服務端交互的方式和路徑。按照經驗我們通常會設計成如下模式:
請求方法 | URL | 含義 |
---|---|---|
GET | /book | 查詢書籍信息 |
POST | /create_book | 創建書籍記錄 |
POST | /update_book | 更新書籍信息 |
POST | /delete_book | 刪除書籍信息 |
我們按照RESTful API設計如下:
請求方法 | URL | 含義 |
---|---|---|
GET | /book | 查詢書籍信息 |
POST | /book | 創建書籍記錄 |
PUT | /book | 更新書籍信息 |
DELETE | /book | 刪除書籍信息 |
c.JSON響應json數據
package main import "github.com/gin-gonic/gin" //結構體 type user struct { Name string `json:"name"` Age int `json:"age"` } //視圖函數 func perosn(c *gin.Context) { var userInfor=user{Name: "張根",Age:18} c.JSON(200,userInfor) } func main(){ r:=gin.Default() r.GET("/person/",perosn) r.Run(":8002") }
2.MVC模板渲染
如果是小型項目、歷史原因、SEO優化我們使用模板渲染,Gin也是支持MTV模式的。
package main import ( "fmt" "github.com/gin-gonic/gin" ) func index(c *gin.Context) { //3.gin 模板渲染 c.HTML(200, "index.html", gin.H{"title": "首頁", "body": "hello"}) } func main() { //1.創建1個默認的路由引擎 router := gin.Default() router.GET("/", index) //2.gin模板解析 router.LoadHTMLGlob("templates/*") //正則匹配templates/所有文件 router.LoadHTMLGlob("templates/**/*") //正則匹配template/目錄/所有文件 err := router.Run(":9001") if err != nil { fmt.Println("gin啟動失敗", err) } }
3.文件上傳
http請求也可以傳輸文件,有時候我們可以使用gin搭建1個ftp服務器。
單個文件上傳
package main import ( "fmt" "github.com/gin-gonic/gin" "path" ) func handleFile(c *gin.Context) { method := c.Request.Method if method == "GET" { c.HTML(200, "file.html", gin.H{}) } if method == "POST" { //1.從客戶端請求中獲取文件 fileObj, err := c.FormFile("localFile") if err != nil { c.JSON(400, gin.H{"err": err.Error()}) return } //2.保存到服務端 fileStorePath := path.Join("./upload/", fileObj.Filename) err = c.SaveUploadedFile(fileObj, fileStorePath) if err != nil { errMsg := fmt.Sprintf("文件保存失敗:%s\n", err.Error()) c.JSON(200, gin.H{"err": errMsg}) } c.JSON(200, gin.H{"data": "上傳成功"}) } } func main() { router := gin.Default() router.Static("/static", "./static") router.LoadHTMLGlob("templates/*") router.GET("/file/", handleFile) router.POST("/file/", handleFile) err := router.Run(":8002") if err != nil { fmt.Println("gin啟動失敗", err) return } }
多個文件上傳
func main() { router := gin.Default() // 處理multipart forms提交文件時默認的內存限制是32 MiB // 可以通過下面的方式修改 // router.MaxMultipartMemory = 8 << 20 // 8 MiB router.POST("/upload", func(c *gin.Context) { // Multipart form form, _ := c.MultipartForm() files := form.File["file"] for index, file := range files { log.Println(file.Filename) dst := fmt.Sprintf("C:/tmp/%s_%d", file.Filename, index) // 上傳文件到指定的目錄 c.SaveUploadedFile(file, dst) } c.JSON(http.StatusOK, gin.H{ "message": fmt.Sprintf("%d files uploaded!", len(files)), }) }) router.Run() }
Gin模板渲染
現在大部分都是前后端分離的架構,除了seo優化我們基本不會使用gin做模板渲染。
1.擴展gin模板函數
package main import ( "fmt" "github.com/gin-gonic/gin" "html/template" ) func index(c *gin.Context) { //3.gin 模板渲染 c.HTML(200, "index.html", gin.H{"title": "首頁", "name": "Martin", "age": "hello", "url": `<a href="https://www.cnblogs.com/sss4/">主頁</a>`}) } func main() { //1.創建1個默認的路由引擎 router := gin.Default() router.GET("/", index) //1.5 gin框架模板自定義模板函數 router.SetFuncMap(template.FuncMap{ "safe": func(safeString string) template.HTML { return template.HTML(safeString) }, }) //2.gin模板解析 //router.LoadHTMLGlob("templates/*") //正則匹配templates/所有文件 router.LoadHTMLGlob("templates/**/*") //正則匹配template/目錄/所有文件 err := router.Run(":9001") if err != nil { fmt.Println("gin啟動失敗", err) } }
模板
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>{{.title}}</title> </head> <body> <ul> <li>{{.name}}</li> <li>{{.age}}</li> <li>{{.url | safe}}</li> </ul> </body> </html>
2.加載靜態文件路徑
package main import ( "fmt" "github.com/gin-gonic/gin" "html/template" ) func index(c *gin.Context) { //5.gin 模板渲染 c.HTML(200, "index.html", gin.H{"title": "首頁", "name": "Martin", "age": "hello", "url": `<a href="https://www.cnblogs.com/sss4/">主頁</a>`}) } func main() { //1.創建1個默認的路由引擎 router := gin.Default() router.GET("/", index) //2.加載靜態文件路徑 .css router.Static("/static","./static") //3. 擴展gin框架模板自定義模板函數 router.SetFuncMap(template.FuncMap{ "safe": func(safeString string) template.HTML { return template.HTML(safeString) }, }) //4.gin模板解析 //router.LoadHTMLGlob("templates/*") //正則匹配templates/所有文件 router.LoadHTMLGlob("templates/**/*") //正則匹配template/目錄/所有文件 err := router.Run(":9001") if err != nil { fmt.Println("gin啟動失敗", err) } }
模板
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>{{.title}}</title> <link rel="stylesheet" href="/static/dist/css/bootstrap.min.css"> <script src="/static/jquery-3.2.1.min.js"></script> <script src="/static/dist/js/bootstrap.min.js"></script> </head> <body> <div class="container"> <table class="table table-hover"> <theader> <tr> <td>姓名</td> <td>年齡</td> <td>主頁</td> </tr> </theader> <tbody> <tr> <td>{{.name}}</td> <td>{{.age}}</td> <td>{{.url|safe}}</td> </tr> </tbody> </table> </div> </body> </html>
3.Gin模板繼承
html/template實現了模板的嵌套和繼承,但是gin不包含此功能。但是我們使用第第三方包。github.com/gin-contrib/multitemplate
Gin 路由組
URL路由太多了就需要分組管理,類似Flask的藍圖、Django里面的include URL。這些都是基於反射實現的。
但是Gin框架中的路由使用的是httprouter這個庫,其基本原理就是構造一個路由地址的前綴樹。
1.單支路由
//所有請求方式都匯聚到handleBook router.Any("/book/", handleBook) //處理404錯誤 router.NoRoute(handle404)
2.路由組
//cmdb路由組 cmdbRouter := router.Group("/cmdb") { cmdbRouter.GET("/list/") cmdbRouter.GET("/hosts/") cmdbRouter.GET("/option/") } //工單路由組 workOrder := router.Group("/works") { workOrder.GET("/daily/") cmdbRouter.GET("/momthly") cmdbRouter.GET("/quarterly") }
3.路由嵌套
雖然gin的路由支持嵌套,但是出於對查詢性能的考慮我們一般都會不會嵌套很多層路由。
//cmdb路由組 cmdbRouter := router.Group("/cmdb") { cmdbRouter.GET("/list/") cmdbRouter.GET("/hosts/") //1.cmdb的主機 hostRouter := cmdbRouter.Group("/host") { //1.1主機的cpu hostRouter.GET("/cup/") //1.2主機的內存 hostRouter.GET("/memory/") //1.3主機的硬盤 hostRouter.GET("/disks/") //1.4主機運行的服務 hostRouter.GET("/process/") //1.5網絡流量 hostRouter.GET("/networks/") } cmdbRouter.GET("/option/") } //2.工單路由組 workOrder := router.Group("/works") { workOrder.GET("/daily/") cmdbRouter.GET("/momthly") cmdbRouter.GET("/quarterly") }
Gin中間件
我們可以在不修改視圖函數的前提下,利用Web框架中攜帶的鈎子函數也就是中間件 做權限控制、登錄認證、權限校驗、數據分頁、記錄日志、耗時統計.........
注意我們的中間件不僅可以設置1個,也根據我們的業務邏輯設置N個,相當於對用戶請求增加了多層過濾。
就像Python里面的多層裝飾器。
1.中間件執行流程
由於http請求包含request、response 2個動作所以中間件是雙行線,中間件的執行流程就像1個遞歸函數的執行過程。
壓棧: 用戶---------> 認證中間件---------> 用戶權限中間件---------> 錯誤處理中間件---------> 視圖函數執行 出棧: 視圖函數執行完畢---------> 錯誤處理中間件---------> 用戶權限中間件---------> 認證中間件---------> 用戶
2.控制中間件執行流程
所為的控制流程我感覺就是設計中間件這個棧里面包含的層層棧針。
我們在彈匣里裝了什么樣的子彈,扣動扳機時就會發射出什么子彈,這樣想會更簡單一些否則很容易被繞進去。
在中間件執行的過程中我們可以控制進棧和出棧流程。
以上代碼執行結果:
m1 in
m2 in
m1 out
調用context.Next(),繼續調用下一個視圖函數進行壓棧。(子彈裝滿彈匣)
調用context.Abort() 阻止繼續調用后續的函數,執行完當前棧針(函數)之后出棧。(1發子彈就夠了)
調用context.Abort() + return,當前位置返回,當前位置之后的代碼都不需要不執行了。(1發啞彈)
package main import ( "fmt" "github.com/gin-gonic/gin" ) //中間件1 func middleWare1(c *gin.Context) { fmt.Println("middleWare1開始----------") c.Next() //調用后續的處理函數 fmt.Println("middleWare1結束----------") } //中間件2 func middleWare2(c *gin.Context) { fmt.Println("middleWare2開始========") c.Abort()//終止后續處理函數的調用,執行完本函數返回 return //更極端一些 到這就結束!(本函數也不需要執行完畢了)。 fmt.Println("middleWare2結束========") } //index視圖函數 func index(c *gin.Context) { fmt.Println("index開始+++++++++") c.JSON(200, gin.H{"data": "ok"}) fmt.Println("index結束+++++++++") } func main() { router := gin.Default() //全局注冊中間件:middleWare1, middleWare2 router.Use(middleWare1, middleWare2) router.GET("/index/", index) err := router.Run(":9001") if err != nil { fmt.Println("Gin啟動失敗", err) } }
3.給單個路由(url)設置中間件
當我們需要對特定的視圖函數增加新功能時,可以給它增加1個中間件。
package main import ( "fmt" "github.com/gin-gonic/gin" "time" ) //中間件1 func middleWare1(c *gin.Context) { fmt.Println("--------------I`m going through middleWare1----------") start := time.Now() c.Next() //調用后續的處理函數 cost := time.Since(start) fmt.Printf("耗時----------%v\n", cost) c.Abort() //終止請求 } //index handlerfunc類型的函數 func index(c *gin.Context) { fmt.Println("--------------I`m going through handlerfunc----------") c.JSON(200, gin.H{"data": "ok"}) c.Next() } //中間件2 func middleWare2(c *gin.Context) { fmt.Println("--------------I`m going through middleWare2----------") c.Next() c.Abort() //請求終止 } func main() { router := gin.Default() //設置中間件流程:middleWare1-----》index----》middleWare2 router.GET("/index/", middleWare1, index, middleWare2) err := router.Run(":9001") if err != nil { fmt.Println("Gin啟動失敗", err) } }
4.全局注冊中間件
如果我們需要每個視圖函數都設置1個中間件,把這一中間件寫到每個視圖函數前面會非常不方便,我們可以使用use進行全局注冊。
package main import ( "fmt" "github.com/gin-gonic/gin" ) //中間件1 func middleWare1(c *gin.Context) { fmt.Println("middleWare1開始----------") c.Next() //調用后續的處理函數 fmt.Println("middleWare1結束----------") //c.Abort() //終止請求 } //中間件2 func middleWare2(c *gin.Context) { fmt.Println("middleWare2開始========") c.Next() fmt.Println("middleWare2結束========") } //index handlerfunc類型的函數 func index(c *gin.Context) { fmt.Println("index開始+++++++++") c.JSON(200, gin.H{"data": "ok"}) fmt.Println("index結束+++++++++") } func main() { router := gin.Default() //全局注冊中間件:middleWare1, middleWare2 router.Use(middleWare1, middleWare2) router.GET("/index/", index) err := router.Run(":9001") if err != nil { fmt.Println("Gin啟動失敗", err) } }
輸出:
驗證web框架里中間件設計思想是的遞歸思想。
middleWare1開始---------- middleWare2開始======== index開始+++++++++ index結束+++++++++ middleWare2結束======== middleWare1結束----------
5.路由組注冊中間件
給路由組注冊中間件有2種寫法
寫法1:
shopGroup := r.Group("/shop", StatCost()) { shopGroup.GET("/index", func(c *gin.Context) {...}) ... }
寫法2:
shopGroup := r.Group("/shop") shopGroup.Use(StatCost()) { shopGroup.GET("/index", func(c *gin.Context) {...}) ... }
6.閉包的中間件
以上我們得知:Gin的中間件是以1種gin.HandlerFunc類型存在,在路由和路由組里進行注冊。
router.GET("/index/", authMiddleWare(false), index)
那我們可以使用閉包將1個開關參數和這個handlerFunc一起包起來。實現對中間進行開關控制比較靈活。
package main import ( "fmt" "github.com/gin-gonic/gin" ) //使用閉包函數返回,gin.HandlerFunc。可以實現對中間進行開關控制,比較靈活。 func authMiddleWare(work bool) gin.HandlerFunc { if work{ //連接數據庫 //其他准備工作 dbDB := "Martin" return func(c *gin.Context) { username := c.Query("username") if username == dbDB { c.Next() } else { c.Abort() c.JSON(403, gin.H{"data": "沒有訪問權限"}) } } } return func(context *gin.Context) { } } //index視圖函數 func index(c *gin.Context) { fmt.Println("index視圖函數開始") c.JSON(200, gin.H{"data": "ok"}) fmt.Println("index視圖函數結束") } func main() { router := gin.Default() router.GET("/index/", authMiddleWare(false), index) err := router.Run(":9001") if err != nil { fmt.Println("Gin啟動失敗", err) } }
7.誇中間件進行傳值
中間件可以有多層,假如我們上游的中間得出的值,如何傳遞到下游中間件呢?。通過上下文content。
當在中間件或handler
中啟動新的goroutine
時,不能使用原始的上下文(c *gin.Context),必須使用其只讀副本(c.Copy()
)。以保證我們傳遞的值是一致的。
c.Set("username", username) currentUser, ok := c.Get("username")
8.gin默認中間件
gin.Defaut生成的路由引擎,默認使用了Logger(), Recovery()的中間件。
//生成的路由引擎,默認使用了Logger(), Recovery()的中間件 gin.Default()
router := gin.New()
Logger:用於記錄日志
Recovery:用於保證在gin 發生錯誤時進程不會終止。

package main import ( "fmt" "github.com/gin-gonic/gin" ) //使用閉包函數返回,gin.HandlerFunc。可以實現對中間進行開關控制,比較靈活。 func authMiddleWare(work bool) gin.HandlerFunc { if work { //連接數據庫 //其他准備工作 dbDB := "Martin" return func(c *gin.Context) { username := c.Query("username") if username == dbDB { //1.在中間中設置值進行傳遞 c.Set("username", username) fmt.Println("-----------", username) c.Next() } else { c.Abort() c.JSON(403, gin.H{"data": "沒有訪問權限"}) } } } return func(context *gin.Context) { } } //index視圖函數 func index(c *gin.Context) { //2.獲取上流傳遞的值 currentUser, ok := c.Get("username") if !ok { currentUser = "anonymous" } fmt.Println("index視圖函數開始") c.JSON(200, gin.H{"data": currentUser}) fmt.Println("index視圖函數結束") } func main() { //生成的路由引擎,默認使用了Logger(), Recovery()的中間件 gin.Default() //router := gin.Default() router := gin.New() router.GET("/index/", authMiddleWare(true), index) err := router.Run(":9001") if err != nil { fmt.Println("Gin啟動失敗", err) } }
Gin設置cookie
1.cookie是server端保存在browser中的用戶信息
2.每客戶端次訪問server端時都會攜帶該域下的cooki信息到server端。
3.不同域名之間的cookie是不共享的。(無法跨域,所以前后端分離的項目只能使用token)