由於本人也是golang初學者,所以本教程停留在gin的使用層面,沒有對其實現細節進行剖析,後面有機會再來進行深入研究。
gin是目前golang的主要web框架之一,之所以選擇這個框架是因為其擁有高效的路由效能,並且有人長期維護,目前github上的star數已經破3W。本教程適用於新手上路。
如何安裝
這裡略過go的安裝,直接進入gin的安裝環節,本文通過govendor(管理包依賴工具)進行安裝:
- 安裝govendor
go get github.com/kardianos/govendor
複製程式碼
- 在gopath下建立專案目錄並進入
mkdir $GOPATH/src/myapp && cd $_
複製程式碼
- 用govendor初始化專案並拉取gin
govendor init
govendor fetch github.com/gin-gonic/gin
複製程式碼
到這裡已經安裝完畢,此時我們可以看到專案中多了一個vendor目錄,此目錄下就是本專案所需要的依賴。
基本用法
gin的語法非常簡單,我們可以一目瞭然,下面的示例建立了一個gin router,使用了預設的中介軟體(logger和recovery)。然後建立了一個"/ping"請求的路由監聽。最後啟動服務,預設監聽8080埠。
// main.go
package main
import "github.com/gin-gonic/gin"
func main() {
r := gin.Default() // 使用預設中介軟體(logger和recovery)
r.GET("/ping", func(c *gin.Context) {
c.JSON(200, gin.H{ // 返回一個JSON,狀態碼是200,gin.H是map[string]interface{}的簡寫
"message": "pong",
})
})
r.Run() // 啟動服務,並預設監聽8080埠
}
複製程式碼
執行命令:
go run main.go
複製程式碼
路由管理
基本用法
請求方法包括:get, post, patch, delete and options。此外還有any,即任何請求方法都會監聽到。
func main() {
router := gin.Default()
router.GET("/someGet", handle)
router.POST("/somePost", handle)
router.PUT("/somePut", handle)
router.DELETE("/someDelete", handle)
router.PATCH("/somePatch", handle)
router.HEAD("/someHead", handle)
router.OPTIONS("/someOptions", handle)
router.ANY("/any", handle)
router.Run()
}
func handle(context *gin.Context) {
context.String(http.StatusOK, "hello world")
}
複製程式碼
分組路由可以通過router.Group:
func main() {
router := gin.Default()
v1 := router.Group("/v1")
{
v1.POST("/login", loginEndpoint)
v1.POST("/submit", submitEndpoint)
v1.POST("/read", readEndpoint)
}
v2 := router.Group("/v2")
{
v2.POST("/login", loginEndpoint)
v2.POST("/submit", submitEndpoint)
v2.POST("/read", readEndpoint)
}
router.Run(":8080")
}
複製程式碼
請求引數獲取
path中的引數
func main() {
router := gin.Default()
// 匹配/user/john
router.GET("/user/:name", func(c *gin.Context) {
name := c.Param("name")
c.String(http.StatusOK, "Hello %s", name)
})
// 匹配/user/john/和/user/john/send
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)
})
router.Run(":8080")
}
複製程式碼
query中的引數
func main() {
router := gin.Default()
// welcome?firstname=Jane&lastname=Doe
router.GET("/user", func(c *gin.Context) {
firstname := c.DefaultQuery("name", "kim") // 獲取query中的name,沒有的話就為kim
lastname := c.Query("age") // 獲取query中的age
c.String(http.StatusOK, "Hello %s %s", firstname, lastname)
})
router.Run(":8080")
}
複製程式碼
multipart/urlencoded form中的引數
func main() {
router := gin.Default()
router.POST("/form_post", func(c *gin.Context) {
message := c.PostForm("age")
nick := c.DefaultPostForm("name", "kim")
c.JSON(200, gin.H{
"status": "posted",
"message": message,
"nick": nick,
})
})
router.Run(":8080")
}
複製程式碼
curl http://127.0.0.1:8080 -X POST -d 'name=john&age=25'
複製程式碼
模型繫結和引數驗證
基本用法
我們已經見識了x-www-form-urlencoded
型別的引數處理,現在越來越多的應用習慣使用JSON來通訊,也就是無論返回的response還是提交的request,其content-type型別都是application/json
的格式。而對於一些舊的web表單頁還是x-www-form-urlencoded
的形式,這就需要我們的伺服器能改hold住這多種content-type的引數了。
由於go是靜態語言,需要先實現定義資料模型,這就需要用到gin的model bind功能了。
gin使用go-playground/validator.v8驗證引數,檢視完整文件。
需要在繫結的欄位上設定tag,比如,繫結格式為json,需要這樣設定json:"fieldname"
。
此外,Gin還提供了兩套繫結方法:
- Must bind
- Methods -
Bind
,BindJSON
,BindXML
,BindQuery
,BindYAML
- Behavior - 這些方法底層使用
MustBindWith
,如果存在繫結錯誤,請求將被以下指令中止c.AbortWithError(400, err).SetType(ErrorTypeBind)
,響應狀態程式碼會被設定為400,請求頭Content-Type
被設定為text/plain; charset=utf-8
。注意,如果你試圖在此之後設定響應程式碼,將會發出一個警告[GIN-debug] [WARNING] Headers were already written. Wanted to override status code 400 with 422
,如果你希望更好地控制行為,請使用ShouldBind
相關的方法
- Methods -
- Should bind
- Methods -
ShouldBind
,ShouldBindJSON
,ShouldBindXML
,ShouldBindQuery
,ShouldBindYAML
- Behavior - 這些方法底層使用 ShouldBindWith,如果存在繫結錯誤,則返回錯誤,開發人員可以正確處理請求和錯誤。
- Methods -
當我們使用繫結方法時,Gin會根據Content-Type推斷出使用哪種繫結器,如果你確定你繫結的是什麼,你可以使用MustBindWith
或者BindingWith
。
你還可以給欄位指定特定規則的修飾符,如果一個欄位用binding:"required"
修飾,並且在繫結時該欄位的值為空,那麼將返回一個錯誤。
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
type Person struct {
Name string `json:"name" binding:"required"` // json格式從name取值,並且該值為必須的
Age int `json:"age" binding:"required,gt=20"` // json格式從age取值,並且該值為必須的,且必須大於20
}
func main() {
router := gin.Default()
router.POST("/test", func(context *gin.Context) {
var person Person
// 這裡我確定傳過來的一定是JSON所以用ShouldBindJSON,否則可以用ShouldBind
if err := context.ShouldBindJSON(&person); err != nil {
context.JSON(http.StatusBadRequest, gin.H{
"error": err.Error(),
})
return
}
context.JSON(http.StatusOK, gin.H{
"success": true,
})
})
router.Run(":3000")
}
複製程式碼
curl http://localhost:3000/test -X POST -d '{"name":"kim","age": 10}'
複製程式碼
自定義驗證器
package main
import (
"net/http"
"reflect"
"time"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
"gopkg.in/go-playground/validator.v8"
)
type Booking struct {
// 這裡的驗證方法為bookabledate
CheckIn time.Time `form:"check_in" binding:"required,bookabledate" time_format:"2006-01-02"`
// gtfield=CheckIn表示大於的欄位為CheckIn
CheckOut time.Time `form:"check_out" binding:"required,gtfield=CheckIn" time_format:"2006-01-02"`
}
func bookableDate(
v *validator.Validate, topStruct reflect.Value, currentStructOrField reflect.Value,
field reflect.Value, fieldType reflect.Type, fieldKind reflect.Kind, param string,
) bool {
// 這裡有兩個知識點,對映和斷言
// 在這裡,field是一個reflect.Type的介面型別變數,通過Interface方法獲得field介面型別變數的真實型別,可以理解為reflect.Value的逆操作
// 在這裡,斷言就是將一個介面型別的變數轉化為time.Time,前提是後者必須實現了前者的介面
// 綜上,這裡就是將field進行了型別轉換
if date, ok := field.Interface().(time.Time); ok {
today := time.Now()
if today.Year() > date.Year() || today.YearDay() > date.YearDay() {
return false
}
}
return true
}
func main() {
route := gin.Default()
// 註冊自定義驗證器
if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
v.RegisterValidation("bookabledate", bookableDate)
}
route.GET("/bookable", getBookable)
route.Run(":8085")
}
func getBookable(c *gin.Context) {
var b Booking
if err := c.ShouldBindWith(&b, binding.Query); err == nil {
c.JSON(http.StatusOK, gin.H{"message": "Booking dates are valid!"})
} else {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
}
}
複製程式碼