本來自己打算繼續(xù)學(xué)下beanFactory源碼的,但是放假了自己也沒什么精神,看源碼又要求注意力很集中,所以想著看點(diǎn)簡(jiǎ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,主要針對(duì)用戶的request和response。gin官網(wǎng),個(gè)人感覺文檔稍顯粗糙,不過勝在支持中文,還是很良心的。
一、安裝和開始
要想使用gin必須要下載和安裝它,切換到自己的工作空間,執(zhí)行g(shù)o命令
go get -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
func main() {
// 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è)是相對(duì)路徑,一個(gè)是多個(gè)handler,即針對(duì)用戶一個(gè)請(qǐng)求地址,我可以指定多個(gè)handler來處理用戶請(qǐng)求。但是一般情況下我們都是一個(gè)handler處理一個(gè)請(qǐng)求。上面的代碼里使用了一個(gè)匿名函數(shù)處理"/hello"請(qǐng)求。然后以JSON格式的數(shù)據(jù)響應(yīng)用戶請(qǐ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é)果如下圖所示:

二、創(chuàng)建demo
接下來創(chuàng)建項(xiàng)目來學(xué)習(xí)gin的使用,主要就是controller的使用,即將用戶請(qǐng)求和handler進(jìn)行映射,然后獲取不同方式請(qǐng)求參數(shù)。構(gòu)建項(xiàng)目結(jié)構(gòu)如下所示

config主要是配置相關(guān)的文件;controller包主要放handler;database包數(shù)據(jù)庫(kù)相關(guān)代碼,因?yàn)槲疫@里沒有用ORM框架,所以只是數(shù)據(jù)庫(kù)連接的代碼;main包下只有main.go一個(gè)文件;model就是數(shù)據(jù)模型,即自己定義的一些結(jié)構(gòu)體;static下放置的是靜態(tài)文件;template包下是html頁面。
剛才上面處理"hello"請(qǐng)求使用的是一個(gè)匿名函數(shù),下面為非匿名函數(shù)來處理,代碼修改成下面:
func main() {
// Engin
router := gin.Default()
router.GET("/hello", hello) // hello函數(shù)處理"/hello"請(qǐng)求
// 指定地址和端口號(hào)
router.Run(":9090")
}
func hello(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,請(qǐng)求路徑都是/user/....,同樣gin也支持,這就是路由組,我們看下官方文檔的示例:
func main() {
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文件,分別處理不同路由組請(qǐng)求,分別作一些不同的操作,另外將每個(gè)路由對(duì)應(yīng)的函數(shù)按照路由組進(jìn)行劃分,另外有兩個(gè)靜態(tài)的html頁面,做form表單提交的操作。gin提供了兩個(gè)方法用戶加載靜態(tài)html,即LoadHTMLGlob()或LoadHTMLFiles(),第一個(gè)方法制定一個(gè)通配符路徑即可,而后面的方法則是需要指定所有需要加載的html文件名稱。修改后代碼如下:
func main() {
// Engin
//router := gin.Default()
router := gin.New()
// 加載html文件,即template包下所有文件
router.LoadHTMLGlob("template/*")
router.GET("/hello", hello)
// 路由組
user := router.Group("/user")
{ // 請(qǐng)求參數(shù)在請(qǐng)求路徑上
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è)請(qǐng)求的路徑對(duì)應(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)于獲取用戶請(qǐng)求參數(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請(qǐng)求,post請(qǐng)求我也寫了兩種形式,一種就是傳統(tǒng)的form表單提交,另外就是json格式參數(shù)提交,等下通過代碼看下。
下面是UserController的代碼內(nèi)容:
func init() {
log.Println(">>>> get database connection start <<<<")
db = database.GetDataBase()
}
// localhost:9090/user/query?id=2&name=hello
func QueryParam(context *gin.Context) {
println(">>>> query user by url params action start <<<<")
id := context.Query("id")
name := context.Request.URL.Query().Get("name")
var u 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)
var user 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
func QueryById (context *gin.Context) {
println(">>>> get user by id and name action start <<<<")
// 獲取請(qǐng)求參數(shù)
id := context.Param("id")
name := context.Param("username")
// 查詢數(shù)據(jù)庫(kù)
rows := db.QueryRow("select username,address,age,mobile,sex from t_user where id = $1 and username = $2",id,name)
var user 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ù)
func InsertNewUser (context *gin.Context) {
println(">>>> insert controller action start <<<<")
var user 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ù)庫(kù)
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)
var count int64
count,err = res.RowsAffected()
checkError(err)
if count != 1 {
context.JSON(200,gin.H{
"success":false,
})
} else {
context.JSON(200,gin.H{
"success":true,
})
}
}
// form表單提交
func PostForm(context *gin.Context) {
println(">>>> bind form post params action start <<<<")
var u 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)
var count int64
count,err = res.RowsAffected()
checkError(err)
if count != 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
func RenderForm(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{})
}
func checkError(e error) {
if e != nil {
log.Fatal(e)
}
}
UserController里面定義一個(gè)init方法,主要獲取數(shù)據(jù)庫(kù)連接,一邊后面的函數(shù)對(duì)數(shù)據(jù)庫(kù)進(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(),然后對(duì)結(jié)構(gòu)體進(jìn)行操作就行了,需要注意一點(diǎn)就是ShouldBindQuery()只能綁定GET請(qǐng)求的查詢參數(shù),POST請(qǐng)求不行。其實(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庫(kù)將數(shù)據(jù)綁定到結(jié)構(gòu)體。最簡(jiǎn)單方法就是調(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ù)具體的請(qǐng)求頭選擇不同綁定引擎去處理,比如用戶請(qǐng)求的Content-Type為"application/json",那么就由JSON的綁定引擎處理,如果為為"application/xml",就由XML綁定引擎處理。這兩個(gè)方法的差別在于ShouldBind方法不會(huì)將response狀態(tài)值設(shè)為400,當(dāng)請(qǐng)求的json參數(shù)無效的時(shí)候,即請(qǐng)求參數(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的相對(duì)template的路徑即可。
FileController主要是處理文件上傳,其實(shí)也沒什么特別內(nèi)容,無非就是單個(gè)上傳還是多個(gè)上傳的問題,另外就是使用base64上傳圖片。代碼如下:
const BASE_NAME = "./static/file/"
func RenderView (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è)文件上傳
func FormUpload (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()
defer file.Close()
bytes,e := ioutil.ReadAll(file)
e = ioutil.WriteFile(BASE_NAME + fh.Filename,bytes,0666)
checkError(e)
if e != nil {
context.JSON(200,gin.H{
"success":false,
})
} else {
context.JSON(200,gin.H{
"success":true,
})
}
}
// 多個(gè)文件上傳
func MultiUpload(context *gin.Context) {
println(">>>> upload file by form action start <<<<")
form,err := context.MultipartForm()
checkError(err)
files := form.File["file"]
var er error
for _,f := range files {
// 使用gin自帶保存文件方法
er = context.SaveUploadedFile(f,BASE_NAME + f.Filename)
checkError(err)
}
if er != nil {
context.JSON(200,gin.H{
"success":false,
})
} else {
context.JSON(200,gin.H{
"success":true,
})
}
}
func Base64Upload (context *gin.Context) {
println(">>>> upload file by base64 string action start <<<<")
bytes,err := ioutil.ReadAll(context.Request.Body)
if err != 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)
if err != 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)如下:
type FileHeader struct {
Filename string
Header textproto.MIMEHeader
Size int64
content []byte
tmpfile string
}
保存文件可以直接使用SaveUploadedFile方法,也可以使用ioutil相關(guān)方法進(jìn)行保存。
MultiUpload多文件上傳,先通過context.MultipartForm()獲取Form對(duì)象,然后根據(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)很久沒有提交過代碼了.......