gin是go語言環境下的一個web框架, 它相似於Martini, 官方聲稱它比Martini有更好的性能, 比Martini快40倍, Ohhhh….看着不錯的樣子, 因此就想記錄一下gin的學習. gin的github代碼在這裏: gin源碼. gin的效率得到如此日新月異, 得益於另外一個開源項目httprouter, 項目地址: httprouter源碼. 下面主要記錄一下gin的使用.html
###1. 安裝gin 使用命令go get github.com/gin-gonic/gin
就能夠. 咱們使用gin的時候引入相應的包就OKimport "github.com/gin-gonic/gin"
.git
###2. 使用方法github
gin服務端代碼是:web
// func1: 處理最基本的GET func func1 (c *gin.Context) { // 回覆一個200OK,在client的http-get的resp的body中獲取數據 c.String(http.StatusOK, "test1 OK") } // func2: 處理最基本的POST func func2 (c *gin.Context) { // 回覆一個200 OK, 在client的http-post的resp的body中獲取數據 c.String(http.StatusOK, "test2 OK") } func main(){ // 註冊一個默認的路由器 router := gin.Default() // 最基本的用法 router.GET("/test1", func1) router.POST("/test2", func2) // 綁定端口是8888 router.Run(":8888") }
客戶端代碼是:json
func main(){ // 調用最基本的GET,並得到返回值 resp,_ := http.Get("http://0.0.0.0:8888/test1") helpRead(resp) // 調用最基本的POST,並得到返回值 resp,_ = http.Post("http://0.0.0.0:8888/test2", "",strings.NewReader("")) helpRead(resp) }
在服務端, 實例化了一個router, 而後使用GET和POST方法分別註冊了兩個服務, 當咱們使用HTTP GET方法的時候會使用GET註冊的函數, 若是使用HTTP POST的方法, 那麼會使用POST註冊的函數. gin支持全部的HTTP的方法例如: GET, POST, PUT, PATCH, DELETE 和 OPTIONS等. 看客戶端中的代碼, 當調用http.Get("http://0.0.0.0:8888/test1")
的時候, 服務端接收到請求, 並根據/test1將請求路由到func1函數進行 處理. 同理, 調用http.Post("http://0.0.0.0:8888/test2", "",strings.NewReader(""))
時候, 會使用func2函數處理. 在func1和func2中, 使用gin.Context填充了一個String的回覆. 固然也支持JSON, XML, HTML等其餘一些格式數據. 當執行c.String或者c.JSON時, 至關於向http的回覆緩衝區寫入了 一些數據. 最後調用router.Run(「:8888」)開始進行監聽,Run的核心代碼是:瀏覽器
func (engine *Engine) Run(addr string) (err error) { debugPrint("Listening and serving HTTP on %s\n", addr) defer func() { debugPrintError(err) }() // 核心代碼 err = http.ListenAndServe(addr, engine) return }
其本質就是http.ListenAndServe(addr, engine).
注意: helpRead函數是用於讀取response的Body的函數, 你能夠本身定義, 本文中此函數定義爲:服務器
// 用於讀取resp的body func helpRead(resp *http.Response) { defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) if err != nil { fmt.Println("ERROR2!: ", err) } fmt.Println(string(body)) }
傳遞參數有幾種方法, 對應到gin使用幾種不一樣的方式來解析.app
對應的服務端代碼爲:框架
// func3: 處理帶參數的path-GET func func3(c *gin.Context) { // 回覆一個200 OK // 獲取傳入的參數 name := c.Param("name") passwd := c.Param("passwd") c.String(http.StatusOK, "參數:%s %s test3 OK", name, passwd) } // func4: 處理帶參數的path-POST func func4(c *gin.Context) { // 回覆一個200 OK // 獲取傳入的參數 name := c.Param("name") passwd := c.Param("passwd") c.String(http.StatusOK, "參數:%s %s test4 OK", name, passwd) } // func5: 注意':'和'*'的區別 func func5(c *gin.Context) { // 回覆一個200 OK // 獲取傳入的參數 name := c.Param("name") passwd := c.Param("passwd") c.String(http.StatusOK, "參數:%s %s test5 OK", name, passwd) } func main(){ router := gin.Default() // TODO:注意':'必需要匹配,'*'選擇匹配,即存在就匹配,不然能夠不考慮 router.GET("/test3/:name/:passwd", func3) router.POST("/test4/:name/:passwd", func4) router.GET("/test5/:name/*passwd", func5) router.Run(":8888") }
客戶端測試代碼是:ide
func main() { // GET傳參數,使用gin的Param解析格式: /test3/:name/:passwd resp,_ = http.Get("http://0.0.0.0:8888/test3/name=TAO/passwd=123") helpRead(resp) // POST傳參數,使用gin的Param解析格式: /test3/:name/:passwd resp,_ = http.Post("http://0.0.0.0:8888/test4/name=PT/passwd=456", "",strings.NewReader("")) helpRead(resp) // 注意Param中':'和'*'的區別 resp,_ = http.Get("http://0.0.0.0:8888/test5/name=TAO/passwd=789") helpRead(resp) resp,_ = http.Get("http://0.0.0.0:8888/test5/name=TAO/") helpRead(resp) }
注意上面定義參數的方法有兩個輔助符號: ‘:’和’*’. 若是使用’:’參數方法, 那麼這個參數是必需要匹配的, 例如上面的router.GET(「/test3/:name/:passwd」, func3), 當請求URL是 相似於http://0.0.0.0:8888/test3/name=TAO/passwd=123這樣的參會被匹配, 若是是http://0.0.0.0:8888/test3/name=TAO 或者http://0.0.0.0:8888/test3/passwd=123是不能匹配的. 可是若是使用’*‘參數, 那麼這個參數是可選的. router.GET(「/test5/:name/*passwd」, func5) 能夠匹配http://0.0.0.0:8888/test5/name=TAO/passwd=789, 也能夠匹配http://0.0.0.0:8888/test5/name=TAO/. 須要注意的一點是, 下面這個URL是否是可以 匹配呢? http://0.0.0.0:8888/test5/name=TAO, 注意TAO後面沒有’/’, 這個其實就要看有沒有一個路由是到http://0.0.0.0:8888/test5/name=TAO路徑的, 若是有, 那麼指定的那個函數進行處理, 若是沒有http://0.0.0.0:8888/test5/name=TAO會被重定向到http://0.0.0.0:8888/test5/name=TAO/, 而後被當前註冊的函數進行處理.
這個相似於正常的URL中的參數傳遞, 先看服務端代碼:
// 使用Query獲取參數 func func6(c *gin.Context) { // 回覆一個200 OK // 獲取傳入的參數 name := c.Query("name") passwd := c.Query("passwd") c.String(http.StatusOK, "參數:%s %s test6 OK", name, passwd) } // 使用Query獲取參數 func func7(c *gin.Context) { // 回覆一個200 OK // 獲取傳入的參數 name := c.Query("name") passwd := c.Query("passwd") c.String(http.StatusOK, "參數:%s %s test7 OK", name, passwd) } func main(){ router := gin.Default() // 使用gin的Query參數形式,/test6?firstname=Jane&lastname=Doe router.GET("/test6", func6) router.POST("/test7", func7) router.Run(":8888") }
客戶端測試代碼是:
func main() { // 使用Query獲取參數形式/test6?firstname=Jane&lastname=Doe resp,_ = http.Get("http://0.0.0.0:8888/test6?name=BBB&passwd=CCC") helpRead(resp) resp,_ = http.Post("http://0.0.0.0:8888/test7?name=DDD&passwd=EEE", "",strings.NewReader("")) helpRead(resp) }
這種方法的參數也是接在URL後面, 形如http://0.0.0.0:8888/test6?name=BBB&passwd=CCC. 服務器可使用name := c.Query(「name」)這種 方法來解析參數.
咱們須要將參數放在請求的Body中傳遞, 而不是URL中. 先看服務端代碼:
// 參數是form中得到,即從Body中得到,忽略URL中的參數 func func8(c *gin.Context) { message := c.PostForm("message") extra := c.PostForm("extra") nick := c.DefaultPostForm("nick", "anonymous") c.JSON(200, gin.H{ "status": "test8:posted", "message": message, "nick": nick, "extra": extra, }) } func main(){ router := gin.Default() // 使用post_form形式,注意必需要設置Post的type, // 同時此方法中忽略URL中帶的參數,全部的參數須要從Body中得到 router.POST("/test8", func8) router.Run(":8888") }
客戶端代碼是:
func main() { // 使用post_form形式,注意必需要設置Post的type,同時此方法中忽略URL中帶的參數,全部的參數須要從Body中得到 resp,_ = http.Post("http://0.0.0.0:8888/test8", "application/x-www-form-urlencoded",strings.NewReader("message=8888888&extra=999999")) helpRead(resp) }
因爲咱們使用了request Body, 那麼就須要指定Body中數據的形式, 此處是form格式, 即application/x-www-form-urlencoded. 常見的幾種http提交數據方式有: application/x-www-form-urlencoded; multipart/form-data; application/json; text/xml. 具體使用請google.
在服務端, 使用message := c.PostForm(「message」)方法解析參數, 而後進行處理.
下面測試從client傳輸文件到server. 傳輸文件須要使用multipart/form-data格式的數據, 全部須要設定Post的類型是multipart/form-data.
首先看服務端代碼:
// 接收client上傳的文件 // 從FormFile中獲取相關的文件data! // 而後寫入本地文件 func func9(c *gin.Context) { // 注意此處的文件名和client處的應該是同樣的 file, header , err := c.Request.FormFile("uploadFile") filename := header.Filename fmt.Println(header.Filename) // 建立臨時接收文件 out, err := os.Create("copy_"+filename) if err != nil { log.Fatal(err) } defer out.Close() // Copy數據 _, err = io.Copy(out, file) if err != nil { log.Fatal(err) } c.String(http.StatusOK, "upload file success") } func main(){ router := gin.Default() // 接收上傳的文件,須要使用 router.POST("/upload", func9) router.Run(":8888") }
客戶端代碼是:
func main() { // 上傳文件POST // 下面構造一個文件buf做爲POST的BODY buf := new(bytes.Buffer) w := multipart.NewWriter(buf) fw,_ := w.CreateFormFile("uploadFile", "images.png") //這裏的uploadFile必須和服務器端的FormFile-name一致 fd,_ := os.Open("images.png") defer fd.Close() io.Copy(fw, fd) w.Close() resp,_ = http.Post("http://0.0.0.0:8888/upload", w.FormDataContentType(), buf) helpRead(resp) }
首先客戶端本地須要有一張」images.png」圖片, 同時須要建立一個Form, 並將field-name命名爲」uploadFile」, file-name命名爲」images.png」. 在服務端, 經過」uploadFile」能夠獲得文件信息. 客戶端繼續將圖片數據copy到建立好的Form中, 將數據數據Post出去, 注意數據的類型指定! 在服務端, 經過file, header , err := c.Request.FormFile(「uploadFile」)得到文件信息, file中就是文件數據, 將其拷貝到本地文件, 完成文件傳輸.
gin內置了幾種數據的綁定例如JSON, XML等. 簡單來講, 即根據Body數據類型, 將數據賦值到指定的結構體變量中. (相似於序列化和反序列化)
看服務端代碼:
// Binding數據 // 注意:後面的form:user表示在form中這個字段是user,不是User, 一樣json:user也是 // 注意:binding:"required"要求這個字段在client端發送的時候必須存在,不然報錯! type Login struct { User string `form:"user" json:"user" binding:"required"` Password string `form:"password" json:"password" binding:"required"` } // bind JSON數據 func funcBindJSON(c *gin.Context) { var json Login // binding JSON,本質是將request中的Body中的數據按照JSON格式解析到json變量中 if c.BindJSON(&json) == nil { if json.User == "TAO" && json.Password == "123" { c.JSON(http.StatusOK, gin.H{"JSON=== status": "you are logged in"}) } else { c.JSON(http.StatusUnauthorized, gin.H{"JSON=== status": "unauthorized"}) } } else { c.JSON(404, gin.H{"JSON=== status": "binding JSON error!"}) } } // 下面測試bind FORM數據 func funcBindForm(c *gin.Context) { var form Login // 本質是將c中的request中的BODY數據解析到form中 // 方法一: 對於FORM數據直接使用Bind函數, 默認使用使用form格式解析,if c.Bind(&form) == nil // 方法二: 使用BindWith函數,若是你明確知道數據的類型 if c.BindWith(&form, binding.Form) == nil{ if form.User == "TAO" && form.Password == "123" { c.JSON(http.StatusOK, gin.H{"FORM=== status": "you are logged in"}) } else { c.JSON(http.StatusUnauthorized, gin.H{"FORM=== status": "unauthorized"}) } } else { c.JSON(404, gin.H{"FORM=== status": "binding FORM error!"}) } } func main(){ router := gin.Default() // 下面測試bind JSON數據 router.POST("/bindJSON", funcBindJSON) // 下面測試bind FORM數據 router.POST("/bindForm", funcBindForm) // 下面測試JSON,XML等格式的rendering router.GET("/someJSON", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"message": "hey, budy", "status": http.StatusOK}) }) router.GET("/moreJSON", func(c *gin.Context) { // 注意:這裏定義了tag指示在json中顯示的是user不是User var msg struct { Name string `json:"user"` Message string Number int } msg.Name = "TAO" msg.Message = "hey, budy" msg.Number = 123 // 下面的在client的顯示是"user": "TAO",不是"User": "TAO" // 因此整體的顯示是:{"user": "TAO", "Message": "hey, budy", "Number": 123 c.JSON(http.StatusOK, msg) }) // 測試發送XML數據 router.GET("/someXML", func(c *gin.Context) { c.XML(http.StatusOK, gin.H{"name":"TAO", "message": "hey, budy", "status": http.StatusOK}) }) router.Run(":8888") }
客戶端代碼:
func main() { // 下面測試binding數據 // 首先測試binding-JSON, // 注意Body中的數據必須是JSON格式 resp,_ = http.Post("http://0.0.0.0:8888/bindJSON", "application/json", strings.NewReader("{\"user\":\"TAO\", \"password\": \"123\"}")) helpRead(resp) // 下面測試bind FORM數據 resp,_ = http.Post("http://0.0.0.0:8888/bindForm", "application/x-www-form-urlencoded", strings.NewReader("user=TAO&password=123")) helpRead(resp) // 下面測試接收JSON和XML數據 resp,_ = http.Get("http://0.0.0.0:8888/someJSON") helpRead(resp) resp,_ = http.Get("http://0.0.0.0:8888/moreJSON") helpRead(resp) resp,_ = http.Get("http://0.0.0.0:8888/someXML") helpRead(resp) }
客戶端發送請求, 在服務端能夠直接使用c.BindJSON綁定到Json結構體上. 或者使用BindWith函數也能夠, 可是須要指定綁定的數據類型, 例如JSON, XML, HTML等. Bind*函數的本質是讀取request中的body數據, 拿BindJSON爲例, 其核心代碼是:
func (_ jsonBinding) Bind(req *http.Request, obj interface{}) error { // 核心代碼: decode請求的body到obj中 decoder := json.NewDecoder(req.Body) if err := decoder.Decode(obj); err != nil { return err } return validate(obj) }
router group是爲了方便前綴相同的URL的管理, 其基本用法以下.
首先看服務端代碼:
// router GROUP - GET測試 func func10(c *gin.Context) { c.String(http.StatusOK, "test10 OK") } func func11(c *gin.Context) { c.String(http.StatusOK, "test11 OK") } // router GROUP - POST測試 func func12(c *gin.Context) { c.String(http.StatusOK, "test12 OK") } func func13(c *gin.Context) { c.String(http.StatusOK, "test13 OK") } func main(){ router := gin.Default() // router Group是爲了將一些前綴相同的URL請求放在一塊兒管理 group1 := router.Group("/g1") group1.GET("/read1", func10) group1.GET("/read2", func11) group2 := router.Group("/g2") group2.POST("/write1", func12) group2.POST("/write2", func13) router.Run(":8888") }
客戶端測試代碼:
func main() { // 下面測試router 的GROUP resp,_ = http.Get("http://0.0.0.0:8888/g1/read1") helpRead(resp) resp,_ = http.Get("http://0.0.0.0:8888/g1/read2") helpRead(resp) resp,_ = http.Post("http://0.0.0.0:8888/g2/write1", "", strings.NewReader("")) helpRead(resp) resp,_ = http.Post("http://0.0.0.0:8888/g2/write2", "", strings.NewReader("")) helpRead(resp) }
在服務端代碼中, 首先建立了一個組group1 := router.Group(「/g1」), 並在這個組下注冊了兩個服務, group1.GET(「/read1」, func10) 和group1.GET(「/read2」, func11), 那麼當使用http://0.0.0.0:8888/g1/read1和http://0.0.0.0:8888/g1/read2訪問時, 是能夠路由 到上面註冊的位置的. 同理對於group2 := router.Group(「/g2」)也是同樣的.
能夠向客戶端展現本地的一些文件信息, 例如顯示某路徑下地文件. 服務端代碼是:
func main(){ router := gin.Default() // 下面測試靜態文件服務 // 顯示當前文件夾下的全部文件/或者指定文件 router.StaticFS("/showDir", http.Dir(".")) router.Static("/files", "/bin") router.StaticFile("/image", "./assets/1.png") router.Run(":8888") }
首先你須要在服務器的路徑下建立一個assert文件夾, 而且放入1.png文件. 若是已經存在, 請忽略.
測試代碼: 請在瀏覽器中輸入0.0.0.0:8888/showDir, 顯示的是服務器當前路徑下地文件信息:
輸入0.0.0.0:8888/files, 顯示的是/bin目錄下地文件信息:
輸入0.0.0.0:8888/image, 顯示的是服務器下地./assets/1.png圖片:
gin支持加載HTML模板, 而後根據模板參數進行配置並返回相應的數據.
看服務端代碼
func main(){ router := gin.Default() // 下面測試加載HTML: LoadHTMLTemplates // 加載templates文件夾下全部的文件 router.LoadHTMLGlob("templates/*") // 或者使用這種方法加載也是OK的: router.LoadHTMLFiles("templates/template1.html", "templates/template2.html") router.GET("/index", func(c *gin.Context) { // 注意下面將gin.H參數傳入index.tmpl中!也就是使用的是index.tmpl模板 c.HTML(http.StatusOK, "index.tmpl", gin.H{ "title": "GIN: 測試加載HTML模板", }) }) router.Run(":8888") }
客戶端測試代碼是:
func main() { // 測試加載HTML模板 resp,_ = http.Get("http://0.0.0.0:8888/index") helpRead(resp) }
在服務端, 咱們須要加載須要的templates, 這裏有兩種方法: 第一種使用LoadHTMLGlob加載全部的正則匹配的模板, 本例中使用的是*, 即匹配全部文件, 因此加載的是 templates文件夾下全部的模板. 第二種使用LoadHTMLFiles加載指定文件. 在本例服務器路徑下有一個templates目錄, 下面有一個index.tmpl模板, 模板的 內容是:
<html> <h1> { { .title } } </h1> </html>
當客戶端請求/index時, 服務器使用這個模板, 並填充相應的參數, 此處參數只有title, 而後將HTML數據返回給客戶端.
你也能夠在瀏覽器請求0.0.0.0:8888/index, 效果以下圖所示:
重定向相對比較簡單, 服務端代碼是:
func main(){ router := gin.Default() // 下面測試重定向 router.GET("/redirect", func(c *gin.Context) { c.Redirect(http.StatusMovedPermanently, "http://shanshanpt.github.io/") }) router.Run(":8888") }
客戶端測試代碼是:
func main() { // 下面測試重定向 resp,_ = http.Get("http://0.0.0.0:8888/redirect") helpRead(resp) }
當咱們請求http://0.0.0.0:8888/redirect的時候, 會重定向到http://shanshanpt.github.io/這個站點.
這裏使用了兩個例子, 一個是logger, 另外一個是BasiAuth, 具體看服務器代碼:
func Logger() gin.HandlerFunc { return func(c *gin.Context) { t := time.Now() // 設置example變量到Context的Key中,經過Get等函數能夠取得 c.Set("example", "12345") // 發送request以前 c.Next() // 發送request以後 latency := time.Since(t) log.Print(latency) // 這個c.Write是ResponseWriter,咱們能夠得到狀態等信息 status := c.Writer.Status() log.Println(status) } } func main(){ router := gin.Default() // 1 router.Use(Logger()) router.GET("/logger", func(c *gin.Context) { example := c.MustGet("example").(string) log.Println(example) }) // 2 // 下面測試BasicAuth()中間件登陸認證 // var secrets = gin.H{ "foo": gin.H{"email": "foo@bar.com", "phone": "123433"}, "austin": gin.H{"email": "austin@example.com", "phone": "666"}, "lena": gin.H{"email": "lena@guapa.com", "phone": "523443"}, } // Group using gin.BasicAuth() middleware // gin.Accounts is a shortcut for map[string]string authorized := router.Group("/admin", gin.BasicAuth(gin.Accounts{ "foo": "bar", "austin": "1234", "lena": "hello2", "manu": "4321", })) // 請求URL: 0.0.0.0:8888/admin/secrets authorized.GET("/secrets", func(c *gin.Context) { // get user, it was set by the BasicAuth middleware user := c.MustGet(gin.AuthUserKey).(string) if secret, ok := secrets[user]; ok { c.JSON(http.StatusOK, gin.H{"user": user, "secret": secret}) } else { c.JSON(http.StatusOK, gin.H{"user": user, "secret": "NO SECRET :("}) } }) router.Run(":8888") }
客戶端測試代碼是:
func main() { // 下面測試使用中間件 resp,_ = http.Get("http://0.0.0.0:8888/logger") helpRead(resp) // 測試驗證權限中間件BasicAuth resp,_ = http.Get("http://0.0.0.0:8888/admin/secrets") helpRead(resp) }
服務端使用Use方法導入middleware, 當請求/logger來到的時候, 會執行Logger(), 而且咱們知道在GET註冊的時候, 同時註冊了匿名函數, 全部請看Logger函數中存在一個c.Next()的用法, 它是取出全部的註冊的函數都執行一遍, 而後再回到本函數中, 因此, 本例中至關因而先執行了 c.Next()即註冊的匿名函數, 而後回到本函數繼續執行. 因此本例的Print的輸出順序是:
log.Println(example)
log.Print(latency)
log.Println(status)
若是將c.Next()放在log.Print(latency)後面, 那麼log.Println(example)和log.Print(latency)執行的順序就調換了. 因此一切都取決於c.Next()執行的位置. c.Next()的核心代碼以下:
// Next should be used only in the middlewares. // It executes the pending handlers in the chain inside the calling handler. // See example in github. func (c *Context) Next() { c.index++ s := int8(len(c.handlers)) for ; c.index < s; c.index++ { c.handlers[c.index](c) } }
它實際上是執行了後面全部的handlers.
關於使用gin.BasicAuth() middleware, 能夠直接使用一個router group進行處理, 本質和logger同樣.
以前全部的測試中, 咱們都是使用router.Run(":8888")
開始執行監聽, 其實還有兩種方法:
// 方法二 http.ListenAndServe(":8888", router) // 方法三: server := &http.Server{ Addr: ":8888", Handler: router, ReadTimeout: 10 * time.Second, WriteTimeout: 10 * time.Second, MaxHeaderBytes: 1 << 20, } server.ListenAndServe()
至此, gin最基本的一些應用都整理完了