本來自己打算繼續(xù)學(xué)下beanFactory源碼的惹资,但是放假了自己也沒什么精神扼脐,看源碼又要求注意力很集中纹腌,所以想著看點(diǎn)簡單點(diǎn)的內(nèi)容吧幌缝,然后就想到了golang的另一個(gè)框架-Gin灸促。假期過后可能就要開啟加班生活了,不是很開心涵卵,昨天收到老大郵件浴栽,我原來項(xiàng)目組基本上解散了,人員分到了不同項(xiàng)目組轿偎,而我到了ebay項(xiàng)目組去做微服務(wù)(如果不用加班我還是期待的)典鸡,自己浪了一個(gè)月也該收收心了。還是回歸正題坏晦,gin框架和前面學(xué)習(xí)的beego框架都是比較流行的框架萝玷,但是beego比較傳統(tǒng),模塊多功能全昆婿,而gin可以看作是一個(gè)單獨(dú)模塊的框架球碉,官方介紹說的是:Gin 是一個(gè) Go (Golang) 語言框架。 它是一個(gè)擁有更好性能的 martini-like API 框架, 由于?httprouter仓蛆,速度提高了近 40 倍汁尺。 如果你是性能和高效的追求者, 那么你會(huì)愛上 Gin。自己感覺gin更像是beego中的controller多律,主要針對用戶的request和response痴突。gin官網(wǎng),個(gè)人感覺文檔稍顯粗糙狼荞,不過勝在支持中文辽装,還是很良心的。
一相味、安裝和開始
要想使用gin必須要下載和安裝它拾积,切換到自己的工作空間,執(zhí)行g(shù)o命令
goget-u?github.com/gin-gonic/gin
但是因?yàn)榫W(wǎng)絡(luò)問題可能會(huì)失敗丰涉,實(shí)在不行就直接通過github下載也可以拓巧。
安裝好之后就可以直接使用了,打開ide創(chuàng)建一個(gè)新的項(xiàng)目helloGin一死,創(chuàng)建main.go
funcmain(){
//?Engin
router?:=?gin.Default()
//router?:=?gin.New()
router.GET("/hello",func(context?*gin.Context){
log.Println(">>>>?hello?gin?start?<<<<")
context.JSON(200,gin.H{
"code":200,
"success":true,
})
})
//?指定地址和端口號(hào)
router.Run("localhost:9090")
在main函數(shù)里面首先通過調(diào)用gin.Default()函數(shù)返回的是一個(gè)Engin指針肛度,Engin代表的是整個(gè)框架的一個(gè)實(shí)例,它包含了多路復(fù)用投慈、中間件和配置的設(shè)置承耿,其實(shí)就是封裝了我們需要的內(nèi)容冠骄。一般創(chuàng)建Engin都是使用Default()或者New(),當(dāng)然Default()本身內(nèi)部也是調(diào)用的New()函數(shù)加袋。
接著調(diào)用Engin的GET方法凛辣,這個(gè)方法兩個(gè)參數(shù),一個(gè)是相對路徑职烧,一個(gè)是多個(gè)handler扁誓,即針對用戶一個(gè)請求地址,我可以指定多個(gè)handler來處理用戶請求蚀之。但是一般情況下我們都是一個(gè)handler處理一個(gè)請求蝗敢。上面的代碼里使用了一個(gè)匿名函數(shù)處理"/hello"請求。然后以JSON格式的數(shù)據(jù)響應(yīng)用戶請求恬总,這個(gè)方法有兩個(gè)參數(shù),第一個(gè)是狀態(tài)肚邢,第二個(gè)是結(jié)果壹堰。我這里直接指定200,表示成功骡湖,或者也可以用http包的常量值http.StatusOK贱纠;gin.H其實(shí)是一個(gè)map的數(shù)據(jù)結(jié)構(gòu),然后將其轉(zhuǎn)成json格式輸出响蕴。
最后是router.Run("localhost:9090")谆焊,這個(gè)方法是指定服務(wù)的主機(jī)和端口號(hào),不過一般直接指定端口號(hào)就行了浦夷。
下面啟動(dòng)項(xiàng)目辖试,并訪問"localhost:9090/hello",訪問結(jié)果如下圖所示:
圖-1.png
二劈狐、創(chuàng)建demo
接下來創(chuàng)建項(xiàng)目來學(xué)習(xí)gin的使用罐孝,主要就是controller的使用,即將用戶請求和handler進(jìn)行映射肥缔,然后獲取不同方式請求參數(shù)莲兢。構(gòu)建項(xiàng)目結(jié)構(gòu)如下所示
圖-2.png
config主要是配置相關(guān)的文件;controller包主要放handler续膳;database包數(shù)據(jù)庫相關(guān)代碼改艇,因?yàn)槲疫@里沒有用ORM框架,所以只是數(shù)據(jù)庫連接的代碼坟岔;main包下只有main.go一個(gè)文件谒兄;model就是數(shù)據(jù)模型,即自己定義的一些結(jié)構(gòu)體社付;static下放置的是靜態(tài)文件舵变;template包下是html頁面酣溃。
剛才上面處理"hello"請求使用的是一個(gè)匿名函數(shù),下面為非匿名函數(shù)來處理纪隙,代碼修改成下面:
funcmain(){
//?Engin
router?:=?gin.Default()
router.GET("/hello",?hello)//?hello函數(shù)處理"/hello"請求
//?指定地址和端口號(hào)
router.Run(":9090")
}
funchello(context?*gin.Context){
println(">>>>?hello?function?start?<<<<")
context.JSON(http.StatusOK,gin.H{
"code":200,
"success":true,
})
}
這樣好了一點(diǎn)點(diǎn)赊豌,但是想想spring controller,一般會(huì)在類上加上一個(gè)@requestMapping注解绵咱,然后方法上也會(huì)加上一個(gè)@requestMapping注解碘饼,之所以在類上加@requestMapping主要是這個(gè)controller處理的是同一類型問題,比如和用戶相關(guān)的controller悲伶,請求路徑都是/user/….艾恼,同樣gin也支持,這就是路由組麸锉,我們看下官方文檔的示例:
funcmain(){
router?:=?gin.Default()
//?Simple?group:?v1
v1?:=?router.Group("/v1")
{
v1.POST("/login",?loginEndpoint)
v1.POST("/submit",?submitEndpoint)
v1.POST("/read",?readEndpoint)
}
//?Simple?group:?v2
v2?:=?router.Group("/v2")
{
v2.POST("/login",?loginEndpoint)
v2.POST("/submit",?submitEndpoint)
v2.POST("/read",?readEndpoint)
}
router.Run(":8080")
}
根據(jù)這個(gè)事例钠绍,將代碼重新構(gòu)建,這里構(gòu)建兩個(gè)路由組花沉。并且在controller包下新建了UserController和FileController文件柳爽,分別處理不同路由組請求,分別作一些不同的操作碱屁,另外將每個(gè)路由對應(yīng)的函數(shù)按照路由組進(jìn)行劃分磷脯,另外有兩個(gè)靜態(tài)的html頁面,做form表單提交的操作娩脾。gin提供了兩個(gè)方法用戶加載靜態(tài)html赵誓,即LoadHTMLGlob()或LoadHTMLFiles(),第一個(gè)方法制定一個(gè)通配符路徑即可柿赊,而后面的方法則是需要指定所有需要加載的html文件名稱俩功。修改后代碼如下:
funcmain(){
//?Engin
//router?:=?gin.Default()
router?:=?gin.New()
//?加載html文件,即template包下所有文件
router.LoadHTMLGlob("template/*")
router.GET("/hello",?hello)
//?路由組
user?:=?router.Group("/user")
{//?請求參數(shù)在請求路徑上
user.GET("/get/:id/:username",controller.QueryById)
user.GET("/query",controller.QueryParam)
user.POST("/insert",controller.InsertNewUser)
user.GET("/form",controller.RenderForm)//?跳轉(zhuǎn)html頁面
user.POST("/form/post",controller.PostForm)
//可以自己添加其他碰声,一個(gè)請求的路徑對應(yīng)一個(gè)函數(shù)
//?...
}
file?:=?router.Group("/file")
{
//?跳轉(zhuǎn)上傳文件頁面
file.GET("/view",controller.RenderView)//?跳轉(zhuǎn)html頁面
//?根據(jù)表單上傳
file.POST("/insert",controller.FormUpload)
file.POST("/multiUpload",controller.MultiUpload)
//?base64上傳
file.POST("/upload",controller.Base64Upload)
}
//?指定地址和端口號(hào)
router.Run(":9090")
}
關(guān)于獲取用戶請求參數(shù)我還是寫了幾種情況绑雄,一是傳統(tǒng)的URL查詢參數(shù),例如:localhost:9090/user/query?id=2&name=hello奥邮;另外一種就是URL路徑參數(shù)万牺,例如localhost:9090/user/2/hello(也是id=2,name=hello)。上面這兩種是get請求洽腺,post請求我也寫了兩種形式脚粟,一種就是傳統(tǒng)的form表單提交,另外就是json格式參數(shù)提交蘸朋,等下通過代碼看下核无。
下面是UserController的代碼內(nèi)容:
funcinit(){
log.Println(">>>>?get?database?connection?start?<<<<")
db?=?database.GetDataBase()
}
//?localhost:9090/user/query?id=2&name=hello
funcQueryParam(context?*gin.Context){
println(">>>>?query?user?by?url?params?action?start?<<<<")
id?:=?context.Query("id")
name?:=?context.Request.URL.Query().Get("name")
varu?model.User
context.Bind(&u)
println(u.Username)
rows?:=?db.QueryRow("select?username,address,age,mobile,sex?from?t_user?where?id?=?$1?and?username?=?$2",id,name)
varuser?model.User
err?:=?rows.Scan(&user.Username,&user.Address,&user.Age,&user.Mobile,&user.Sex)
checkError(err)
checkError(err)
context.JSON(200,gin.H{
"result":user,
})
}
//?localhost:9090/user/get/2/hello
funcQueryById(context?*gin.Context){
println(">>>>?get?user?by?id?and?name?action?start?<<<<")
//?獲取請求參數(shù)
id?:=?context.Param("id")
name?:=?context.Param("username")
//?查詢數(shù)據(jù)庫
rows?:=?db.QueryRow("select?username,address,age,mobile,sex?from?t_user?where?id?=?$1?and?username?=?$2",id,name)
varuser?model.User
err?:=?rows.Scan(&user.Username,&user.Address,&user.Age,&user.Mobile,&user.Sex)
checkError(err)
context.JSON(200,gin.H{
"result":user,
})
}
//?json格式數(shù)據(jù)
funcInsertNewUser(context?*gin.Context){
println(">>>>?insert?controller?action?start?<<<<")
varuser?model.User
//?使用ioutile讀取二進(jìn)制數(shù)據(jù)
//bytes,err?:=?ioutil.ReadAll(context.Request.Body)
//if?err?!=?nil?{
//??log.Fatal(err)
//}
//err?=?json.Unmarshal(bytes,&user)
//?直接將結(jié)構(gòu)體和提交的json參數(shù)作綁定
err?:=?context.ShouldBindJSON(&user)
//?寫入數(shù)據(jù)庫
res,err?:=?db.Exec("insert?into?t_user?(username,sex,address,mobile,age)?values?($1,$2,$3,$4,$5)",
&user.Username,&user.Sex,&user.Address,&user.Mobile,&user.Age)
varcountint64
count,err?=?res.RowsAffected()
checkError(err)
ifcount?!=1{
context.JSON(200,gin.H{
"success":false,
})
}else{
context.JSON(200,gin.H{
"success":true,
})
}
}
//?form表單提交
funcPostForm(context?*gin.Context){
println(">>>>?bind?form?post?params?action?start?<<<<")
varu?model.User
//?綁定參數(shù)到結(jié)構(gòu)體
context.Bind(&u)
res,err?:=?db.Exec("insert?into?t_user?(username,sex,address,mobile,age)?values?($1,$2,$3,$4,$5)",
&u.Username,&u.Sex,&u.Address,&u.Mobile,&u.Age)
varcountint64
count,err?=?res.RowsAffected()
checkError(err)
ifcount?!=1{
context.JSON(200,gin.H{
"success":false,
})
}else{
//context.JSON(200,gin.H{
//??"success":true,
//})
//?重定向
context.Redirect(http.StatusMovedPermanently,"/file/view")
}
}
//?跳轉(zhuǎn)html
funcRenderForm(context?*gin.Context){
println(">>>>?render?to?html?action?start?<<<<")
context.Header("Content-Type","text/html;?charset=utf-8")
context.HTML(200,"insertUser.html",gin.H{})
}
funccheckError(e?error){
ife?!=nil{
log.Fatal(e)
}
}
UserController里面定義一個(gè)init方法,主要獲取數(shù)據(jù)庫連接藕坯,一邊后面的函數(shù)對數(shù)據(jù)庫進(jìn)行操作团南。
在QueryParam函數(shù)中噪沙,獲取URL查詢參數(shù)其實(shí)用多種方法,一種直接使用context.Query("參數(shù)名稱")吐根,另外就是context.Request.URL.Query().Get("參數(shù)名稱")正歼,但是明顯第二個(gè)更麻煩一點(diǎn)。此外還有一種就是將參數(shù)綁定到結(jié)構(gòu)體拷橘,context.Bind()或者context.ShouldBind()或者ShouldBindQuery()局义,然后對結(jié)構(gòu)體進(jìn)行操作就行了,需要注意一點(diǎn)就是ShouldBindQuery()只能綁定GET請求的查詢參數(shù)冗疮,POST請求不行萄唇。其實(shí)使用哪種方式還是看個(gè)人習(xí)慣,參數(shù)少的話感覺第一種更直觀一些术幔。
QueryById函數(shù)獲取的是URL路徑參數(shù)另萤,和QueryParam獲取方法不同,可以通過context.Param("參數(shù)名稱")獲取诅挑,后來看gin文檔四敞,發(fā)現(xiàn)也提供了一種參數(shù)綁定的方法,即context.ShouldBindUri()揍障,這個(gè)方法也會(huì)把結(jié)構(gòu)體和URL路徑參數(shù)做一個(gè)綁定目养。
InsertNewUser函數(shù)俩由,獲取的是提交的JSON格式參數(shù)毒嫡,使用rest client可以模擬,獲取參數(shù)也不止一種幻梯,可以使用比較基礎(chǔ)的方法獲取兜畸,即使用ioutil.ReadAll(context.Request.Body),讀取字節(jié)流,然后使用go內(nèi)置的json庫將數(shù)據(jù)綁定到結(jié)構(gòu)體碘梢。最簡單方法就是調(diào)用ShouldBindJSON()咬摇,將用戶提交的JSON參數(shù)綁定結(jié)構(gòu)體。
PostForm函數(shù)就是一個(gè)傳統(tǒng)的form表單提交煞躬,使用context.Bind()或者context.ShouldBind()就好了肛鹏。
關(guān)于Bind和ShouldBind,其實(shí)這兩個(gè)方法基本上都是一樣的恩沛,根據(jù)具體的請求頭選擇不同綁定引擎去處理在扰,比如用戶請求的Content-Type為"application/json",那么就由JSON的綁定引擎處理雷客,如果為為"application/xml"芒珠,就由XML綁定引擎處理。這兩個(gè)方法的差別在于ShouldBind方法不會(huì)將response狀態(tài)值設(shè)為400搅裙,當(dāng)請求的json參數(shù)無效的時(shí)候皱卓,即請求參數(shù)無法綁定到結(jié)構(gòu)體裹芝。
RenderForm函數(shù)主要是跳轉(zhuǎn)到html頁面,當(dāng)時(shí)這里遇到一個(gè)問題娜汁,就是context.HTML方法嫂易,指定具體html頁面,因?yàn)閙ain函數(shù)使用時(shí)是router.LoadHTMLGlob("template/*")存炮,我覺得可以理解指定了具體html的前綴炬搭,所以跳轉(zhuǎn)時(shí)只需要html的相對template的路徑即可。
FileController主要是處理文件上傳穆桂,其實(shí)也沒什么特別內(nèi)容宫盔,無非就是單個(gè)上傳還是多個(gè)上傳的問題,另外就是使用base64上傳圖片享完。代碼如下:
constBASE_NAME?="./static/file/"
funcRenderView(context?*gin.Context){
println(">>>>?render?to?file?upload?view?action?start?<<<<")
context.Header("Content-Type","text/html;?charset=utf-8")
context.HTML(200,"fileUpload.html",gin.H{})
}
//?單個(gè)文件上傳
funcFormUpload(context?*gin.Context){
println(">>>>?upload?file?by?form?action?start?<<<<")
fh,err?:=?context.FormFile("file")
checkError(err)
//context.SaveUploadedFile(fh,BASE_NAME?+?fh.Filename)
file,err?:=?fh.Open()
deferfile.Close()
bytes,e?:=?ioutil.ReadAll(file)
e?=?ioutil.WriteFile(BASE_NAME?+?fh.Filename,bytes,0666)
checkError(e)
ife?!=nil{
context.JSON(200,gin.H{
"success":false,
})
}else{
context.JSON(200,gin.H{
"success":true,
})
}
}
//?多個(gè)文件上傳
funcMultiUpload(context?*gin.Context){
println(">>>>?upload?file?by?form?action?start?<<<<")
form,err?:=?context.MultipartForm()
checkError(err)
files?:=?form.File["file"]
varer?error
for_,f?:=rangefiles?{
//?使用gin自帶保存文件方法
er?=?context.SaveUploadedFile(f,BASE_NAME?+?f.Filename)
checkError(err)
}
ifer?!=nil{
context.JSON(200,gin.H{
"success":false,
})
}else{
context.JSON(200,gin.H{
"success":true,
})
}
}
funcBase64Upload(context?*gin.Context){
println(">>>>?upload?file?by?base64?string?action?start?<<<<")
bytes,err?:=?ioutil.ReadAll(context.Request.Body)
iferr?!=nil{
log.Fatal(err)
}
strs?:=?strings.Split(string(bytes),",")
head?:=?strs[0]
body?:=?strs[1]
println(head?+"?|?"+?body)
start?:=?strings.LastIndex(head,"/")
end?:=?strings.LastIndex(head,";")
tp?:=?head[start?+1:end]
err?=?ioutil.WriteFile(BASE_NAME?+?strconv.Itoa(time.Now().Nanosecond())?+"."+?tp,[]byte(body),0666)
checkError(err)
//bys,err?:=?base64.StdEncoding.DecodeString(string(bytes))
//err?=?ioutil.WriteFile("./static/file/"?+?strconv.Itoa(time.Now().Nanosecond()),bys,0666)
iferr?!=nil{
context.JSON(200,gin.H{
"success":false,
})
}else{
context.JSON(200,gin.H{
"success":true,
})
}
}
FormUpload函數(shù)處理單個(gè)文件上傳灼芭,先從context.FormFile("file")獲取文件,獲取到的是一個(gè)FileHeader指針般又,F(xiàn)ileHeader封裝了文件內(nèi)容彼绷、名稱、類型茴迁、大小等信息寄悯,結(jié)構(gòu)如下:
typeFileHeaderstruct{
Filenamestring
Header???textproto.MIMEHeader
Sizeint64
content?[]byte
tmpfilestring
}
保存文件可以直接使用SaveUploadedFile方法,也可以使用ioutil相關(guān)方法進(jìn)行保存堕义。
MultiUpload多文件上傳猜旬,先通過context.MultipartForm()獲取Form對象,然后根據(jù)參數(shù)名獲取到多個(gè)FileHeader指針倦卖,接下去保存文件和單個(gè)上傳是一樣的洒擦。
Base64Upload函數(shù)本來是想通過使用base64上傳圖片,函數(shù)內(nèi)先獲取整個(gè)字符串怕膛,然后分割成head和body熟嫩,然后判斷圖片類型,最后使用ioutil.WriteFile保存文件褐捻,但是實(shí)際操作好像出了點(diǎn)問題掸茅,文件保存到本地打開顯示內(nèi)容丟失,不知道是怎么回事柠逞。
三昧狮、總結(jié)
當(dāng)然gin內(nèi)容不止這些,還有一些中間件的內(nèi)容也是值得一看的边苹,比如BasicAuth陵且、Logger等等,但是總感覺gin似乎太輕了一點(diǎn),基本上就是一個(gè)MVC框架慕购,還是要結(jié)合其他框架使用聊疲。beego感覺更好一些,但是MVC這部分好像gin更強(qiáng)大點(diǎn)沪悲,總之都很優(yōu)秀吧获洲,畢竟GitHub上star那么多。今天的學(xué)習(xí)就到這里了殿如,本次學(xué)習(xí)的代碼已經(jīng)上傳到我的GitHub贡珊,我已經(jīng)很久沒有提交過代碼了…….