Golang框架之gin

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

 

[安装]

go get -u github.com/gin-gonic/gin

 

基础使用:

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 - 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

// var person = Person{
// Name: "default", // 设置参数默认值, 若不存在该值的时候
// }
// 这里我确定传过来的一定是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")
}

 

 中间件中的程序终端Abort

func Auth() {
  return func(c *gin.Context){
    fmt.Println("before...")

    c.Abort()  // 终端当前请求,不会执行到controller层中具体逻辑, 其他中间件仍会执行,和该中间件中之后的逻辑
    return  // 当前中间件中剩余逻辑不会被执行

   fmt.Println("after...")
  }  
}

  

 [日志设置]

使用:

    //f, err := os.Create("gin.log")  // 重启之后会清空该文件
    f, err := os.OpenFile("gin.log", os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
    gin.DefaultWriter = io.MultiWriter(f, os.Stdout)

    //fErr, _ := os.Create("gin-err.log")  // 重启之后会清空该文件
    // 内部发生panic错误的时候,并且没有进行异常捕获的情况下,会将其输入到该文件, 若存在异常中间件的话,则自定义进行处理即可,并不会输出到该文件中了
    fErr, err := os.OpenFile("gin-err.log", os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
    gin.DefaultErrorWriter = io.MultiWriter(fErr, os.Stderr)

    // 设置运行模式
    gin.SetMode(cfg.App.RunMode)
    // HTTP Server
    handler := gin.New()
    ...

 

[运行模式]

系统支持三种运行模式: debug,test,release

  debug: 会输出所有信息, 包含启动服务的时候的提示信息以及路由信息等

       test:不会输出服务启动信息与路由信息, 之后输出路由访问信息

     release: 显示信息与test模式的一致

判断运行模式方式:

func IsRelease() bool {
    return gin.Mode() == gin.ReleaseMode

 

 

 

 

 

 

 

 

 

 

 

 

posted @ 2022-08-15 17:46  X-Wolf  阅读(3439)  评论(0编辑  收藏  举报