golang web框架——gin使用教程(一)

維和土豆發表於2019-09-26

由於本人也是golang初學者,所以本教程停留在gin的使用層面,沒有對其實現細節進行剖析,後面有機會再來進行深入研究。

gin是目前golang的主要web框架之一,之所以選擇這個框架是因為其擁有高效的路由效能,並且有人長期維護,目前github上的star數已經破3W。本教程適用於新手上路。

如何安裝

這裡略過go的安裝,直接進入gin的安裝環節,本文通過govendor(管理包依賴工具)進行安裝:

  1. 安裝govendor
go get github.com/kardianos/govendor
複製程式碼
  1. 在gopath下建立專案目錄並進入
mkdir $GOPATH/src/myapp && cd $_
複製程式碼
  1. 用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 - BindBindJSONBindXMLBindQueryBindYAML
    • 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相關的方法
  • Should bind
    • Methods - ShouldBindShouldBindJSONShouldBindXMLShouldBindQueryShouldBindYAML
    • Behavior - 這些方法底層使用 ShouldBindWith,如果存在繫結錯誤,則返回錯誤,開發人員可以正確處理請求和錯誤。

當我們使用繫結方法時,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()})
	}
}
複製程式碼

相關文章