所謂框架
框架一直是敏捷開發(fā)中的利器,能讓開發(fā)者很快的上手并做出應(yīng)用,甚至有的時候,脫離了框架,一些開發(fā)者都不會寫程序了。成長總不會一蹴而就,從寫出程序獲取成就感,再到精通框架,快速構(gòu)造應(yīng)用,當(dāng)這些方面都得心應(yīng)手的時候,可以嘗試改造一些框架,或是自己創(chuàng)造一個。
曾經(jīng)我以為Python世界里的框架已經(jīng)夠多了,后來發(fā)現(xiàn)相比golang簡直小巫見大巫。golang提供的net/http庫已經(jīng)很好了,對于http的協(xié)議的實現(xiàn)非常好,基于此再造框架,也不會是難事,因此生態(tài)中出現(xiàn)了很多框架。既然構(gòu)造框架的門檻變低了,那么低門檻同樣也會帶來質(zhì)量參差不齊的框架。
考察了幾個框架,通過其github的活躍度,維護(hù)的team,以及生產(chǎn)環(huán)境中的使用率。發(fā)現(xiàn)Gin還是一個可以學(xué)習(xí)的輕巧框架。
Gin
Gin是一個golang的微框架,封裝比較優(yōu)雅,API友好,源碼注釋比較明確,已經(jīng)發(fā)布了1.0版本。具有快速靈活,容錯方便等特點(diǎn)。其實對于golang而言,web框架的依賴要遠(yuǎn)比Python,Java之類的要小。自身的net/http足夠簡單,性能也非常不錯??蚣芨袷且恍┏S煤瘮?shù)或者工具的集合。借助框架開發(fā),不僅可以省去很多常用的封裝帶來的時間,也有助于團(tuán)隊的編碼風(fēng)格和形成規(guī)范。
下面就Gin的用法做一個簡單的介紹。
首先需要安裝,安裝比較簡單,使用go get即可:
go get gopkg.in/gin-gonic/gin.v1
gin的版本托管再 gopkg的網(wǎng)站上。我在安裝的過程中,gokpg卡住了,后來不得不根據(jù)gin里的godep的文件,把響應(yīng)的源碼從github上下載,然后copy到對應(yīng)的目錄。
關(guān)于golang的包管理和依賴,我們以后再討論。
Hello World
使用Gin實現(xiàn)Hello world非常簡單,創(chuàng)建一個router,然后使用其Run的方法:
import (
"gopkg.in/gin-gonic/gin.v1"
"net/http"
)
func main(){
router := gin.Default()
router.GET("/", func(c *gin.Context) {
c.String(http.StatusOK, "Hello World")
})
router.Run(":8000")
}
簡單幾行代碼,就能實現(xiàn)一個web服務(wù)。使用gin的Default方法創(chuàng)建一個路由handler。然后通過HTTP方法綁定路由規(guī)則和路由函數(shù)。不同于net/http庫的路由函數(shù),gin進(jìn)行了封裝,把request和response都封裝到gin.Context的上下文環(huán)境。最后是啟動路由的Run方法監(jiān)聽端口。麻雀雖小,五臟俱全。當(dāng)然,除了GET方法,gin也支持POST,PUT,DELETE,OPTION等常用的restful方法。
restful路由
gin的路由來自httprouter庫。因此httprouter具有的功能,gin也具有,不過gin不支持路由正則表達(dá)式:
func main(){
router := gin.Default()
router.GET("/user/:name", func(c *gin.Context) {
name := c.Param("name")
c.String(http.StatusOK, "Hello %s", name)
})
}
冒號:加上一個參數(shù)名組成路由參數(shù)??梢允褂胏.Params的方法讀取其值。當(dāng)然這個值是字串string。諸如/user/rsj217,和/user/hello都可以匹配,而/user/和/user/rsj217/不會被匹配。
? ~ curl http://127.0.0.1:8000/user/rsj217
Hello rsj217% ? ~ curl http://127.0.0.1:8000/user/rsj217/
404 page not found% ? ~ curl http://127.0.0.1:8000/user/
404 page not found%
除了:,gin還提供了*號處理參數(shù),*號能匹配的規(guī)則就更多。
func main(){
router := gin.Default()
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)
})
}
訪問效果如下
? ~ curl http://127.0.0.1:8000/user/rsj217/
rsj217 is /% ? ~ curl http://127.0.0.1:8000/user/rsj217/中國
rsj217 is /中國%
query string參數(shù)與body參數(shù)
web提供的服務(wù)通常是client和server的交互。其中客戶端向服務(wù)器發(fā)送請求,除了路由參數(shù),其他的參數(shù)無非兩種,查詢字符串query string和報文體body參數(shù)。所謂query string,即路由用,用?以后連接的key1=value2&key2=value2的形式的參數(shù)。當(dāng)然這個key-value是經(jīng)過urlencode編碼。
query string
對于參數(shù)的處理,經(jīng)常會出現(xiàn)參數(shù)不存在的情況,對于是否提供默認(rèn)值,gin也考慮了,并且給出了一個優(yōu)雅的方案:
func main(){
router := gin.Default()
router.GET("/welcome", func(c *gin.Context) {
firstname := c.DefaultQuery("firstname", "Guest")
lastname := c.Query("lastname")
c.String(http.StatusOK, "Hello %s %s", firstname, lastname)
})
router.Run()
}
使用c.DefaultQuery方法讀取參數(shù),其中當(dāng)參數(shù)不存在的時候,提供一個默認(rèn)值。使用Query方法讀取正常參數(shù),當(dāng)參數(shù)不存在的時候,返回空字串:
? ~ curl http://127.0.0.1:8000/welcome
Hello Guest % ? ~ curl http://127.0.0.1:8000/welcome\?firstname\=中國
Hello 中國 % ? ~ curl http://127.0.0.1:8000/welcome\?firstname\=中國\&lastname\=天朝
Hello 中國 天朝% ? ~ curl http://127.0.0.1:8000/welcome\?firstname\=\&lastname\=天朝
Hello 天朝%
? ~ curl http://127.0.0.1:8000/welcome\?firstname\=%E4%B8%AD%E5%9B%BD
Hello 中國 %
之所以使用中文,是為了說明urlencode。注意,當(dāng)firstname為空字串的時候,并不會使用默認(rèn)的Guest值,空值也是值,DefaultQuery只作用于key不存在的時候,提供默認(rèn)值。
body
http的報文體傳輸數(shù)據(jù)就比query string稍微復(fù)雜一點(diǎn),常見的格式就有四種。例如application/json,application/x-www-form-urlencoded, application/xml和multipart/form-data。后面一個主要用于圖片上傳。json格式的很好理解,urlencode其實也不難,無非就是把query string的內(nèi)容,放到了body體里,同樣也需要urlencode。默認(rèn)情況下,c.PostFROM解析的是x-www-form-urlencoded或from-data的參數(shù)。
func main(){
router := gin.Default()
router.POST("/form_post", func(c *gin.Context) {
message := c.PostForm("message")
nick := c.DefaultPostForm("nick", "anonymous")
c.JSON(http.StatusOK, gin.H{
"status": gin.H{
"status_code": http.StatusOK,
"status": "ok",
},
"message": message,
"nick": nick,
})
})
}
與get處理query參數(shù)一樣,post方法也提供了處理默認(rèn)參數(shù)的情況。同理,如果參數(shù)不存在,將會得到空字串。
? ~ curl -X POST http://127.0.0.1:8000/form_post -H "Content-Type:application/x-www-form-urlencoded" -d "message=hello&nick=rsj217" | python -m json.tool
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 104 100 79 100 25 48555 15365 --:--:-- --:--:-- --:--:-- 79000
{
"message": "hello",
"nick": "rsj217",
"status": {
"status": "ok",
"status_code": 200
}
}
前面我們使用c.String返回響應(yīng),顧名思義則返回string類型。content-type是plain或者text。調(diào)用c.JSON則返回json數(shù)據(jù)。其中g(shù)in.H封裝了生成json的方式,是一個強(qiáng)大的工具。使用golang可以像動態(tài)語言一樣寫字面量的json,對于嵌套json的實現(xiàn),嵌套gin.H即可。
發(fā)送數(shù)據(jù)給服務(wù)端,并不是post方法才行,put方法一樣也可以。同時querystring和body也不是分開的,兩個同時發(fā)送也可以:
func main(){
router := gin.Default()
router.PUT("/post", func(c *gin.Context) {
id := c.Query("id")
page := c.DefaultQuery("page", "0")
name := c.PostForm("name")
message := c.PostForm("message")
fmt.Printf("id: %s; page: %s; name: %s; message: %s \n", id, page, name, message)
c.JSON(http.StatusOK, gin.H{
"status_code": http.StatusOK,
})
})
}
上面的例子,展示了同時使用查詢字串和body參數(shù)發(fā)送數(shù)據(jù)給服務(wù)器。
文件上傳
上傳單個文件
前面介紹了基本的發(fā)送數(shù)據(jù),其中multipart/form-data轉(zhuǎn)用于文件上傳。gin文件上傳也很方便,和原生的net/http方法類似,不同在于gin把原生的request封裝到c.Request中了。
func main(){
router := gin.Default()
router.POST("/upload", func(c *gin.Context) {
name := c.PostForm("name")
fmt.Println(name)
file, header, err := c.Request.FormFile("upload")
if err != nil {
c.String(http.StatusBadRequest, "Bad request")
return
}
filename := header.Filename
fmt.Println(file, err, filename)
out, err := os.Create(filename)
if err != nil {
log.Fatal(err)
}
defer out.Close()
_, err = io.Copy(out, file)
if err != nil {
log.Fatal(err)
}
c.String(http.StatusCreated, "upload successful")
})
router.Run(":8000")
}
使用c.Request.FormFile解析客戶端文件name屬性。如果不傳文件,則會拋錯,因此需要處理這個錯誤。一種方式是直接返回。然后使用os的操作,把文件數(shù)據(jù)復(fù)制到硬盤上。
使用下面的命令可以測試上傳,注意upload為c.Request.FormFile指定的參數(shù),其值必須要是絕對路徑:
curl -X POST http://127.0.0.1:8000/upload -F "upload=@/Users/ghost/Desktop/pic.jpg" -H "Content-Type: multipart/form-data"
上傳多個文件
單個文件上傳很簡單,別以為多個文件就會很麻煩。依葫蘆畫瓢,所謂多個文件,無非就是多一次遍歷文件,然后一次copy數(shù)據(jù)存儲即可。下面只寫handler,省略main函數(shù)的初始化路由和開啟服務(wù)器監(jiān)聽了:
router.POST("/multi/upload", func(c *gin.Context) {
err := c.Request.ParseMultipartForm(200000)
if err != nil {
log.Fatal(err)
}
formdata := c.Request.MultipartForm
files := formdata.File["upload"]
for i, _ := range files { /
file, err := files[i].Open()
defer file.Close()
if err != nil {
log.Fatal(err)
}
out, err := os.Create(files[i].Filename)
defer out.Close()
if err != nil {
log.Fatal(err)
}
_, err = io.Copy(out, file)
if err != nil {
log.Fatal(err)
}
c.String(http.StatusCreated, "upload successful")
}
})
與單個文件上傳類似,只不過使用了c.Request.MultipartForm得到文件句柄,再獲取文件數(shù)據(jù),然后遍歷讀寫。
使用curl上傳
curl -X POST http://127.0.0.1:8000/multi/upload -F "upload=@/Users/ghost/Desktop/pic.jpg" -F "upload=@/Users/ghost/Desktop/journey.png" -H "Content-Type: multipart/form-data"
表單上傳
上面我們使用的都是curl上傳,實際上,用戶上傳圖片更多是通過表單,或者ajax和一些requests的請求完成。下面展示一下web的form表單如何上傳。
我們先要寫一個表單頁面,因此需要引入gin如何render模板。前面我們見識了c.String和c.JSON。下面就來看看c.HTML方法。
首先需要定義一個模板的文件夾。然后調(diào)用c.HTML渲染模板,可以通過gin.H給模板傳值。至此,無論是String,JSON還是HTML,以及后面的XML和YAML,都可以看到Gin封裝的接口簡明易用。
創(chuàng)建一個文件夾templates,然后再里面創(chuàng)建html文件upload.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>upload</title>
</head>
<body>
<h3>Single Upload</h3>
<form action="/upload", method="post" enctype="multipart/form-data">
<input type="text" value="hello gin" />
<input type="file" name="upload" />
<input type="submit" value="upload" />
</form>
<h3>Multi Upload</h3>
<form action="/multi/upload", method="post" enctype="multipart/form-data">
<input type="text" value="hello gin" />
<input type="file" name="upload" />
<input type="file" name="upload" />
<input type="submit" value="upload" />
</form>
</body>
</html>
upload 很簡單,沒有參數(shù)。一個用于單個文件上傳,一個用于多個文件上傳。
router.LoadHTMLGlob("templates/*")
router.GET("/upload", func(c *gin.Context) {
c.HTML(http.StatusOK, "upload.html", gin.H{})
})
使用LoadHTMLGlob定義模板文件路徑。
參數(shù)綁定
我們已經(jīng)見識了x-www-form-urlencoded類型的參數(shù)處理,現(xiàn)在越來越多的應(yīng)用習(xí)慣使用JSON來通信,也就是無論返回的response還是提交的request,其content-type類型都是application/json的格式。而對于一些舊的web表單頁還是x-www-form-urlencoded的形式,這就需要我們的服務(wù)器能改hold住這多種content-type的參數(shù)了。
Python的世界里很好解決,畢竟動態(tài)語言不需要實現(xiàn)定義數(shù)據(jù)模型。因此可以寫一個裝飾器將兩個格式的數(shù)據(jù)封裝成一個數(shù)據(jù)模型。golang中要處理并非易事,好在有g(shù)in,他們的model bind功能非常強(qiáng)大。
type User struct {
Username string `form:"username" json:"username" binding:"required"`
Passwd string `form:"passwd" json:"passwd" bdinding:"required"`
Age int `form:"age" json:"age"`
}
func main(){
router := gin.Default()
router.POST("/login", func(c *gin.Context) {
var user User
var err error
contentType := c.Request.Header.Get("Content-Type")
switch contentType {
case "application/json":
err = c.BindJSON(&user)
case "application/x-www-form-urlencoded":
err = c.BindWith(&user, binding.Form)
}
if err != nil {
fmt.Println(err)
log.Fatal(err)
}
c.JSON(http.StatusOK, gin.H{
"user": user.Username,
"passwd": user.Passwd,
"age": user.Age,
})
})
}
先定義一個User模型結(jié)構(gòu)體,然后針對客戶端的content-type,一次使BindJSON和BindWith方法。
? ~ curl -X POST http://127.0.0.1:8000/login -H "Content-Type:application/x-www-form-urlencoded" -d "username=rsj217&passwd=123&age=21" | python -m json.tool
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 79 100 46 100 33 41181 29543 --:--:-- --:--:-- --:--:-- 46000
{
"age": 21,
"passwd": "123",
"username": "rsj217"
}
? ~ curl -X POST http://127.0.0.1:8000/login -H "Content-Type:application/x-www-form-urlencoded" -d "username=rsj217&passwd=123&new=21" | python -m json.tool
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 78 100 45 100 33 37751 27684 --:--:-- --:--:-- --:--:-- 45000
{
"age": 0,
"passwd": "123",
"username": "rsj217"
}
? ~ curl -X POST http://127.0.0.1:8000/login -H "Content-Type:application/x-www-form-urlencoded" -d "username=rsj217&new=21" | python -m json.tool
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0curl: (52) Empty reply from server
No JSON object could be decoded
可以看到,結(jié)構(gòu)體中,設(shè)置了binding標(biāo)簽的字段(username和passwd),如果沒傳會拋錯誤。非banding的字段(age),對于客戶端沒有傳,User結(jié)構(gòu)會用零值填充。對于User結(jié)構(gòu)沒有的參數(shù),會自動被忽略。
改成json的效果類似:
? ~ curl -X POST http://127.0.0.1:8000/login -H "Content-Type:application/json" -d '{"username": "rsj217", "passwd": "123", "age": 21}' | python -m json.tool
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 96 100 46 100 50 32670 35511 --:--:-- --:--:-- --:--:-- 50000
{
"age": 21,
"passwd": "123",
"username": "rsj217"
}
? ~ curl -X POST http://127.0.0.1:8000/login -H "Content-Type:application/json" -d '{"username": "rsj217", "passwd": "123", "new": 21}' | python -m json.tool
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 95 100 45 100 50 49559 55066 --:--:-- --:--:-- --:--:-- 50000
{
"age": 0,
"passwd": "123",
"username": "rsj217"
}
? ~ curl -X POST http://127.0.0.1:8000/login -H "Content-Type:application/json" -d '{"username": "rsj217", "new": 21}' | python -m json.tool
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0curl: (52) Empty reply from server
No JSON object could be decoded
? ~ curl -X POST http://127.0.0.1:8000/login -H "Content-Type:application/json" -d '{"username": "rsj217", "passwd": 123, "new": 21}' | python -m json.tool
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0curl: (52) Empty reply from server
No JSON object could be decoded
使用json還需要注意一點(diǎn),json是有數(shù)據(jù)類型的,因此對于
{"passwd": "123"}和{"passwd": 123}是不同的數(shù)據(jù)類型,解析需要符合對應(yīng)的數(shù)據(jù)類型,否則會出錯。
當(dāng)然,gin還提供了更加高級方法,c.Bind,它會更加content-type自動推斷是bind表單還是json的參數(shù)。
router.POST("/login", func(c *gin.Context) {
var user User
err := c.Bind(&user)
if err != nil {
fmt.Println(err)
log.Fatal(err)
}
c.JSON(http.StatusOK, gin.H{
"username": user.Username,
"passwd": user.Passwd,
"age": user.Age,
})
})
多格式渲染
既然請求可以使用不同的content-type,響應(yīng)也如此。通常響應(yīng)會有html,text,plain,json和xml等。
gin提供了很優(yōu)雅的渲染方法。到目前為止,我們已經(jīng)見識了c.String, c.JSON,c.HTML,下面介紹一下c.XML。
router.GET("/render", func(c *gin.Context) {
contentType := c.DefaultQuery("content_type", "json")
if contentType == "json" {
c.JSON(http.StatusOK, gin.H{
"user": "rsj217",
"passwd": "123",
})
} else if contentType == "xml" {
c.XML(http.StatusOK, gin.H{
"user": "rsj217",
"passwd": "123",
})
}
})
結(jié)果如下:
? ~ curl http://127.0.0.1:8000/render\?content_type\=json
{"passwd":"123","user":"rsj217"}
? ~ curl http://127.0.0.1:8000/render\?content_type\=xml
<map><user>rsj217</user><passwd>123</passwd></map>%
重定向
gin對于重定向的請求,相當(dāng)簡單。調(diào)用上下文的Redirect方法:
router.GET("/redict/google", func(c *gin.Context) {
c.Redirect(http.StatusMovedPermanently, "https://google.com")
})
分組路由
熟悉Flask的同學(xué)應(yīng)該很了解藍(lán)圖分組。Flask提供了藍(lán)圖用于管理組織分組api。gin也提供了這樣的功能,讓你的代碼邏輯更加模塊化,同時分組也易于定義中間件的使用范圍。
v1 := router.Group("/v1")
v1.GET("/login", func(c *gin.Context) {
c.String(http.StatusOK, "v1 login")
})
v2 := router.Group("/v2")
v2.GET("/login", func(c *gin.Context) {
c.String(http.StatusOK, "v2 login")
})
訪問效果如下:
? ~ curl http://127.0.0.1:8000/v1/login
v1 login% ? ~ curl http://127.0.0.1:8000/v2/login
v2 login%
middleware中間件
golang的net/http設(shè)計的一大特點(diǎn)就是特別容易構(gòu)建中間件。gin也提供了類似的中間件。需要注意的是中間件只對注冊過的路由函數(shù)起作用。對于分組路由,嵌套使用中間件,可以限定中間件的作用范圍。中間件分為全局中間件,單個路由中間件和群組中間件。
全局中間件
先定義一個中間件函數(shù):
func MiddleWare() gin.HandlerFunc {
return func(c *gin.Context) {
fmt.Println("before middleware")
c.Set("request", "clinet_request")
c.Next()
fmt.Println("before middleware")
}
}
該函數(shù)很簡單,只會給c上下文添加一個屬性,并賦值。后面的路由處理器,可以根據(jù)被中間件裝飾后提取其值。需要注意,雖然名為全局中間件,只要注冊中間件的過程之前設(shè)置的路由,將不會受注冊的中間件所影響。只有注冊了中間件一下代碼的路由函數(shù)規(guī)則,才會被中間件裝飾。
router.Use(MiddleWare())
{
router.GET("/middleware", func(c *gin.Context) {
request := c.MustGet("request").(string)
req, _ := c.Get("request")
c.JSON(http.StatusOK, gin.H{
"middile_request": request,
"request": req,
})
})
}
使用router裝飾中間件,然后在/middlerware即可讀取request的值,注意在router.Use(MiddleWare())代碼以上的路由函數(shù),將不會有被中間件裝飾的效果。
使用花括號包含被裝飾的路由函數(shù)只是一個代碼規(guī)范,即使沒有被包含在內(nèi)的路由函數(shù),只要使用router進(jìn)行路由,都等于被裝飾了。想要區(qū)分權(quán)限范圍,可以使用組返回的對象注冊中間件。
? ~ curl http://127.0.0.1:8000/middleware
{"middile_request":"clinet_request","request":"clinet_request"}
如果沒有注冊就使用MustGet方法讀取c的值將會拋錯,可以使用Get方法取而代之。
上面的注冊裝飾方式,會讓所有下面所寫的代碼都默認(rèn)使用了router的注冊過的中間件。
單個路由中間件
當(dāng)然,gin也提供了針對指定的路由函數(shù)進(jìn)行注冊。
router.GET("/before", MiddleWare(), func(c *gin.Context) {
request := c.MustGet("request").(string)
c.JSON(http.StatusOK, gin.H{
"middile_request": request,
})
})
把上述代碼寫在 router.Use(Middleware())之前,同樣也能看見/before被裝飾了中間件。
群組中間件
群組的中間件也類似,只要在對于的群組路由上注冊中間件函數(shù)即可:
authorized := router.Group("/", MyMiddelware())
// 或者這樣用:
authorized := router.Group("/")
authorized.Use(MyMiddelware())
{
authorized.POST("/login", loginEndpoint)
}
群組可以嵌套,因為中間件也可以根據(jù)群組的嵌套規(guī)則嵌套。
中間件實踐
中間件最大的作用,莫過于用于一些記錄log,錯誤handler,還有就是對部分接口的鑒權(quán)。下面就實現(xiàn)一個簡易的鑒權(quán)中間件。
router.GET("/auth/signin", func(c *gin.Context) {
cookie := &http.Cookie{
Name: "session_id",
Value: "123",
Path: "/",
HttpOnly: true,
}
http.SetCookie(c.Writer, cookie)
c.String(http.StatusOK, "Login successful")
})
router.GET("/home", AuthMiddleWare(), func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"data": "home"})
})
登錄函數(shù)會設(shè)置一個session_id的cookie,注意這里需要指定path為/,不然gin會自動設(shè)置cookie的path為/auth,一個特別奇怪的問題。/homne的邏輯很簡單,使用中間件AuthMiddleWare注冊之后,將會先執(zhí)行AuthMiddleWare的邏輯,然后才到/home的邏輯。
AuthMiddleWare的代碼如下:
func AuthMiddleWare() gin.HandlerFunc {
return func(c *gin.Context) {
if cookie, err := c.Request.Cookie("session_id"); err == nil {
value := cookie.Value
fmt.Println(value)
if value == "123" {
c.Next()
return
}
}
c.JSON(http.StatusUnauthorized, gin.H{
"error": "Unauthorized",
})
c.Abort()
return
}
}
從上下文的請求中讀取cookie,然后校對cookie,如果有問題,則終止請求,直接返回,這里使用了c.Abort()方法。
In [7]: resp = requests.get('http://127.0.0.1:8000/home')
In [8]: resp.json()
Out[8]: {u'error': u'Unauthorized'}
In [9]: login = requests.get('http://127.0.0.1:8000/auth/signin')
In [10]: login.cookies
Out[10]: <RequestsCookieJar[Cookie(version=0, name='session_id', value='123', port=None, port_specified=False, domain='127.0.0.1', domain_specified=False, domain_initial_dot=False, path='/', path_specified=True, secure=False, expires=None, discard=True, comment=None, comment_url=None, rest={'HttpOnly': None}, rfc2109=False)]>
In [11]: resp = requests.get('http://127.0.0.1:8000/home', cookies=login.cookies)
In [12]: resp.json()
Out[12]: {u'data': u'home'}
異步協(xié)程
golang的高并發(fā)一大利器就是協(xié)程。gin里可以借助協(xié)程實現(xiàn)異步任務(wù)。因為涉及異步過程,請求的上下文需要copy到異步的上下文,并且這個上下文是只讀的。
router.GET("/sync", func(c *gin.Context) {
time.Sleep(5 * time.Second)
log.Println("Done! in path" + c.Request.URL.Path)
})
router.GET("/async", func(c *gin.Context) {
cCp := c.Copy()
go func() {
time.Sleep(5 * time.Second)
log.Println("Done! in path" + cCp.Request.URL.Path)
}()
})
在請求的時候,sleep5秒鐘,同步的邏輯可以看到,服務(wù)的進(jìn)程睡眠了。異步的邏輯則看到響應(yīng)返回了,然后程序還在后臺的協(xié)程處理。
自定義router
gin不僅可以使用框架本身的router進(jìn)行Run,也可以配合使用net/http本身的功能:
func main() {
router := gin.Default()
http.ListenAndServe(":8080", router)
}
或者
func main() {
router := gin.Default()
s := &http.Server{
Addr: ":8000",
Handler: router,
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
MaxHeaderBytes: 1 << 20,
}
s.ListenAndServe()
}
當(dāng)然還有一個優(yōu)雅的重啟和結(jié)束進(jìn)程的方案。后面將會探索使用supervisor管理golang的進(jìn)程。
總結(jié)
Gin是一個輕巧而強(qiáng)大的golang web框架。涉及常見開發(fā)的功能,我們都做了簡單的介紹。關(guān)于服務(wù)的啟動,請求參數(shù)的處理和響應(yīng)格式的渲染,以及針對上傳和中間件鑒權(quán)做了例子。更好的掌握來自實踐,同時gin的源碼注釋很詳細(xì),可以閱讀源碼了解更多詳細(xì)的功能和魔法特性。
文中的部分代碼