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

20,497 阅读5分钟

由于本人也是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()})
	}
}