​gin​​是目前golang的主要web框架之一,之所以选择这个框架是因为其拥有高效的路由性能,并且有人长期维护,目前github上的star数已经破3W。

 

[安装]

go get

基础使用:

package main

import (
"github.com/gin-gonic/gin"
"net/http"
)

func main() {
r := gin.Default()
r.GET("/ping", func(c *gin.Context){
c.JSON(http.StatusOK, gin.H{
"messag": "pong",
})
})

r.Run(":8099")
}

运行服务: go run main.go

 

其中默认使用中间件: logger 和recovery

路由:

请求方法包括: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")
}

 

模型绑定与参数校验:

我们已经见识了​​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​​相关的方法
  • Should bind
  • Methods -​​ShouldBind​​​,​​ShouldBindJSON​​​,​​ShouldBindXML​​​,​​ShouldBindQuery​​​,​​ShouldBindYAML​
  • 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")
}

自定义验证器:

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()})
}
}

 

 中间件:

中间件可以在我们接受到一个http请求时,在handle之前或者handle之后做一些处理。通常,在handle之前,我们可以通过中间件很方便地进行校验,如果再handle之后,我们可以对response进行一些调整。

 

 

// 创建一个不包含中间件的路由器
gin.New()
// 使用自定义中间件或者gin提供的中间件
gin.use(gin.Logger())

可代替:
gin.Default()

其实gin默认使用了Logger和Recovery两个中间件,然后也是在内部调用了New:

// gin.go

func Default() *Engine {
debugPrintWARNINGDefault()
engine := New()
engine.Use(Logger(), Recovery()) // 使用了Logger和Recovery两个中间件
return engine
}

 

 

我们对这两个中间件做一个简单的了解:

  • Logger中间件可以让我们做打印的一些自定义配置
  • Recovery中间件可以让我们从崩溃中恢复

 

func main() {

logfile, _ := os.Create("./logs/gin.log")

// 这里将log输出到指定文件
// 注意这个配置一定要在gin.Default()之前
gin.DefaultWriter = io.MultiWriter(logfile, os.Stdout)

router := gin.Default()

// 这里分别使用两个中间件
router.Use(gin.Logger())
router.Use(gin.Recovery())

router.POST("/test", func(context *gin.Context) {
var person Person
if err := context.ShouldBind(&person); err != nil {
context.JSON(http.StatusBadRequest, gin.H{
"error": err.Error(),
})
return
}
context.JSON(http.StatusOK, gin.H{
"success": true,
})
})

router.Run(":3000")
}

 

 自定义中间件:

// recovery.go

这里只要返回一个HandlerFunc类型即可
func Recovery() HandlerFunc {
return RecoveryWithWriter(DefaultErrorWriter)
}


// gin.go

HandlerFunc就是一个参数为*context的函数
type HandlerFunc func(*Context)

 

 我们来写一个IP鉴权的中间件,假设我们的需求是只有白名单中的ip才可以访问服务器,那么我们可以这么实现:

ipauth.go

// ipauth.go

func Auth() gin.HandlerFunc {
return func(context *gin.Context) {
// 定义ip白名单
whiteList := []string{
"127.0.0.1",
}

ip := context.ClientIP()

flag := false

for _, host := range whiteList {
if ip == host {
flag = true
break
}
}

if !flag {
context.String(http.StatusNetworkAuthenticationRequired, "your ip is not trusted: %s", ip)
context.Abort()
}

}
}

 

 main.go

// main.go

func main() {

router := gin.New()

router.Use(ipauth.Auth())

router.GET("/test", func(context *gin.Context) {
context.JSON(http.StatusOK, gin.H{
"success": true,
})
})

router.Run(":3000")
}

 

 

Group中使用中间件

此外,我们的中间件可以不全局使用,而只针对部分的group:

func main() {

router := gin.Default()

// 定义了group
authorized := router.Group("/auth", ipauth.Auth())

// 对上面这个group进行路由绑定
authorized.GET("/write", handle)

router.GET("/read", handle)

router.Run(":3000")
}

func handle(context *gin.Context) {
context.JSON(http.StatusOK, gin.H{
"success": true,
})
}

 

单个路由使用中间件:

func main() {
router := gin.Default()

// 注册一个路由,使用了 middleware1,middleware2 两个中间件
router.GET("/someGet", middleware1, middleware2, handler)

// 默认绑定 :8080
router.Run()
}

func handler(c *gin.Context) {
log.Println("exec handler")
}

func middleware1(c *gin.Context) {
log.Println("exec middleware1")

//你可以写一些逻辑代码

// 执行该中间件之后的逻辑
c.Next()
}

func middleware2(c *gin.Context) {
log.Println("arrive at middleware2")
// 执行该中间件之前,先跳到流程的下一个方法
c.Next()
// 流程中的其他逻辑已经执行完了
log.Println("exec middleware2")

//你可以写一些逻辑代码

 

可以看出,中间件的写法和路由的 Handler 几乎是一样的,只是多调用​​c.Next()​​​。正是有个​​c.Next()​​​,我们可以在中间件中控制调用逻辑的变化,看下面的 middleware2 代码。在 middleware2中,执行到 ​​c.Next()​​时,Gin 会直接跳到流程的下一个方法中,等到这个方法执行完后,才会回来接着执行 middleware2 剩下的代码。

所以请求上面注册的路由 url /someGet ,请求先到达middleware1,然后到达 middleware2,但此时 middleware2调用了 ​​c.Next()​​,所以 middleware2的代码并没有执行,而是跳到了 handler ,等 handler执行完成后,跳回到 middleware2,执行 middleware2剩下的代码。



在中间件中使用goroutines

在中间件或处理程序中启动新的goroutine时,你不应该使用其中的原始上下文,你必须使用只读副本

func main() {
r := gin.Default()

r.GET("/long_async", func(c *gin.Context) {
// 创建要在goroutine中使用的副本
cCp := c.Copy()
go func() {
// simulate a long task with time.Sleep(). 5 seconds
time.Sleep(5 * time.Second)

// 这里使用你创建的副本
log.Println("Done! in path " + cCp.Request.URL.Path)
}()
})

r.Run(":3000")
}