轉發自:http://shanshanpt.github.io/2016/05/03/go-gin.html
gin是go語言環境下的一個web框架, 它類似于Martini, 官方聲稱它比Martini有更好的性能, 比Martini快40倍, Ohhhh….看著不錯的樣子, 所以就想記錄一下gin的學習. gin的github代碼在這里:gin源碼. gin的效率獲得如此突飛猛進, 得益于另一個開源項目httprouter, 項目地址:httprouter源碼. 下面主要記錄一下gin的使用.
###1. 安裝gin 使用命令go get github.com/gin-gonic/gin就可以. 我們使用gin的時候引入相應的包就OKimport "github.com/gin-gonic/gin".
###2. 使用方法
<1> 一種最簡單的使用GET/POST方法
gin服務端代碼是:
// func1: 處理最基本的GETfunc func1(c*gin.Context){// 回復一個200OK,在client的http-get的resp的body中獲取數據c.String(http.StatusOK,"test1 OK")}// func2: 處理最基本的POSTfunc 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)// 綁定端口是8888router.Run(":8888")}
客戶端代碼是:
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(addrstring)(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的bodyfunc helpRead(resp*http.Response){defer resp.Body.Close()body,err:=ioutil.ReadAll(resp.Body)iferr!=nil{fmt.Println("ERROR2!: ",err)}fmt.Println(string(body))}
<2> 傳遞參數
傳遞參數有幾種方法, 對應到gin使用幾種不同的方式來解析.
**第一種:**使用gin.Context中的Param方法解析
對應的服務端代碼為:
// func3: 處理帶參數的path-GETfunc 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-POSTfunc 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")}
客戶端測試代碼是:
func main(){// GET傳參數,使用gin的Param解析格式: /test3/:name/:passwdresp,_=http.Get("http://0.0.0.0:8888/test3/name=TAO/passwd=123")helpRead(resp)// POST傳參數,使用gin的Param解析格式: /test3/:name/:passwdresp,_=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/, 然后被當前注冊的函數進行處理.
**第二種:**使用gin.Context中的Query方法解析
這個類似于正常的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=Doerouter.GET("/test6",func6)router.POST("/test7",func7)router.Run(":8888")}
客戶端測試代碼是:
func main(){// 使用Query獲取參數形式/test6?firstname=Jane&lastname=Doeresp,_=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”)這種 方法來解析參數.
**第三種:**使用gin.Context中的PostForm方法解析
我們需要將參數放在請求的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”)方法解析參數, 然后進行處理.
<3> 傳輸文件
下面測試從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.Filenamefmt.Println(header.Filename)// 創建臨時接收文件out,err:=os.Create("copy_"+filename)iferr!=nil{log.Fatal(err)}deferout.Close()// Copy數據_,err=io.Copy(out,file)iferr!=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的BODYbuf:=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中就是文件數據, 將其拷貝到本地文件, 完成文件傳輸.
<4> binding數據
gin內置了幾種數據的綁定例如JSON, XML等. 簡單來說, 即根據Body數據類型, 將數據賦值到指定的結構體變量中. (類似于序列化和反序列化)
看服務端代碼:
// Binding數據// 注意:后面的form:user表示在form中這個字段是user,不是User, 同樣json:user也是// 注意:binding:"required"要求這個字段在client端發送的時候必須存在,否則報錯!typeLoginstruct{Userstring`form:"user" json:"user" binding:"required"`Passwordstring`form:"password" json:"password" binding:"required"`}// bind JSON數據func funcBindJSON(c*gin.Context){varjsonLogin// binding JSON,本質是將request中的Body中的數據按照JSON格式解析到json變量中ifc.BindJSON(&json)==nil{ifjson.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){varformLogin// 本質是將c中的request中的BODY數據解析到form中// 方法一: 對于FORM數據直接使用Bind函數, 默認使用使用form格式解析,if c.Bind(&form) == nil// 方法二: 使用BindWith函數,如果你明確知道數據的類型ifc.BindWith(&form,binding.Form)==nil{ifform.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等格式的renderingrouter.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不是Uservarmsgstruct{Namestring`json:"user"`MessagestringNumberint}msg.Name="TAO"msg.Message="hey, budy"msg.Number=123// 下面的在client的顯示是"user": "TAO",不是"User": "TAO"http:// 所以總體的顯示是:{"user": "TAO", "Message": "hey, budy", "Number": 123c.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,objinterface{})error{// 核心代碼: decode請求的body到obj中decoder:=json.NewDecoder(req.Body)iferr:=decoder.Decode(obj);err!=nil{returnerr}returnvalidate(obj)}
<5> router group
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 的GROUPresp,_=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”)也是一樣的.
<6> 靜態文件服務
可以向客戶端展示本地的一些文件信息, 例如顯示某路徑下地文件. 服務端代碼是:
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圖片:
<7> 加載模板templates
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模板, 模板的 內容是:
{ { .title } }
當客戶端請求/index時, 服務器使用這個模板, 并填充相應的參數, 此處參數只有title, 然后將HTML數據返回給客戶端.
你也可以在瀏覽器請求0.0.0.0:8888/index, 效果如下圖所示:

<8> 重定向
重定向相對比較簡單, 服務端代碼是:
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/這個站點.
<9> 使用middleware
這里使用了兩個例子, 一個是logger, 另一個是BasiAuth, 具體看服務器代碼:
funcLogger()gin.HandlerFunc{returnfunc(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()// 1router.Use(Logger())router.GET("/logger",func(c*gin.Context){example:=c.MustGet("example").(string)log.Println(example)})// 2// 下面測試BasicAuth()中間件登錄認證//varsecrets=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]stringauthorized:=router.Group("/admin",gin.BasicAuth(gin.Accounts{"foo":"bar","austin":"1234","lena":"hello2","manu":"4321",}))// 請求URL: 0.0.0.0:8888/admin/secretsauthorized.GET("/secrets",func(c*gin.Context){// get user, it was set by the BasicAuth middlewareuser:=c.MustGet(gin.AuthUserKey).(string)ifsecret,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)// 測試驗證權限中間件BasicAuthresp,_=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
它其實是執行了后面所有的handlers.
關于使用gin.BasicAuth() middleware, 可以直接使用一個router group進行處理, 本質和logger一樣.
<10> 綁定http server
之前所有的測試中, 我們都是使用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()