Web服务(二):Gin框架

简介

Gin 框架是由 Go 语言编写,基于 net/http 包 封装的一个 Web 框架。

Gin 核心的路由功能是通过 定制版的 HttpRouter 来实现的,具有很高的路由性能。

Web 服务基础功能

1. HTTP / HTTPS支持

因为 Gin 框架是基于 net/http 包封装的一个 Web 框架,所以它天然就支持 HTTP / HTTPS。

通过下面方式开启一个 HTTP 服务

insecureServer := &http.Server {
    Addr :         ":8080", 
    Handle:        router(),
    ReadTimeout:   5 * time.Second,
    WriteTimeout:  10 * time.Second,
}

...

err := insecureServer.ListenAndServe()

通过下面方式开启一个 HTTPS 服务

secureServer := &http.Server {
    Addr:         ":8443",
    Handler:      router(),
    ReadTimeout:  5 * time.Second,
    WriteTimeout: 10 * time.Second,
}

...

err := secureServer.ListenAndServeTLS("server.pem", "server.key")

2. 路由匹配

Gin 框架支持两种路由匹配规则:精确匹配 和 模糊匹配。

2.1 精确匹配

例如:路由为 /products/:name

匹配情况如下:

路径匹配情况
/products/iphone12
/products/xiaomi8
/products/xiaomi8/music×
/products/×

2.2 模糊匹配

例如:路由为 /products/*name

匹配情况如下:

3. 路由分组

Gin 通过 Group 函数 实现了 路由分组的功能。

(1)可以将 相同版本 的路由分为一组。

(2)可以将 相同 RESTful 资源 的路由分为一组。

(3)通过路由分组,可以对相同分组的路由做统一处理。

// 统一处理,给所有属于 v1 分组的路由都添加 gin.BasicAuth 中间件,以实现认证功能。
// v1 分组
v1 := router.Group("/v1", gin.BasicAuth(gin.Accounts{"foo": "var", "admin": "pass"})) {
    // 路由匹配
    productv1 := v1.Group("/products") {
        productv1.POST("", productHandler.Create)
        productv1.GET(":name", productHandler.Get)
    }

    // 路由匹配
    orderv1 := v1.Group("/orders") {
        orderv1.POST("", orderHandler.Create)
        orderv2.GET(":name", orderHandler.Get)
    }
}

// v2 分组
v2 := router.Group("/v2", gin.BasicAuth(gin.Accounts{"foo": "var", "admin": "pass"})) {
    productv2.POST("", productHandler.Create)
    productv2.GET(":name", productHandler.Get)
}

4. 一进程多服务

下面代码实现了两个相同的服务,分别监听在不同的端口。

注意,为了不阻塞启动第二个服务,需要把 ListenAndServe 函数放在 goroutine 中执行,并调用 eg.Wait() 来阻塞程序进程,从而让两个 HTTP 服务在 goroutine 中持续监听端口,并提供服务。

var eg errgroup.Group
insecureServe := &http.Server{...}
secureServe := &http.Server{...}

eg.Go(func() error {
    err := insecureServer.ListenAndServe()
    if err := nil && err != http.ErrServerClosed {
        log.Fatal(err)
    }
    return err
})


eg.Go(func() error {
    err := secureServer.ListenAndServeTLS("server.pem", "server.key")
    if err != nil && err != http.ErrServerClosed {
        log.Fatal(err)
    }
    return err
})

if err := eg.Wait(); err != nil {
    log.Fatal(err)
}

5. 参数解析、参数校验、逻辑处理、返回结果

一个 Web 服务应该具有 参数解析、参数校验、逻辑处理、返回结果 4类功能。

HTTP 的请求参数可以存在不同的位置,Gin 是如何解析的呢?

5.1 HTTP 具有以下5种参数类型

  • 路径参数(path)

        例如:/user/:name

        其中, name 就是路径参数。

  • 查询字符串参数(query)

        例如:/welcome?firstname=tian&lastname=dh

        其中, firstname 和 lastname 就是查询字符串参数。

  • 表单参数(form)

        例如:curl -X POST -F 'username=tian' -F 'password=123456' http://domain.com/login

        其中,username、password 是表单参数。

  • HTTP头参数(header)

        例如: curl -X POST -H 'Content-Type: application/json' -d '{"username": "admin", "password": "123456"}' http://domain.com/login

        其中,Content-Type 就是 HTTP 头参数。     

  • 消息体参数(body)

        例如: curl -X POST -H 'Content-Type: application/json' -d '{"username": "admin", "password": "123456"}' http://domain.com/login

        其中,username、password 是消息体参数。

5.2 参数解析

方式一:直接读取某个参数的值

        使用 c.Param() 函数

方式二:把同类 HTTP 参数 绑定到一个 Go 结构体中

        Gin 在绑定参数时,通过结构体的 tag 来判断要绑定哪类参数到结构体中。

gin.Default().GET("/:name/:id", nil)

// 方式一
name := c.Param("name")

// 方式二
type Person struct {
    ID   string  `uri: "id"    binding: "required,uuid"`
    Name string  `uri: "name"  binding: "required"`
}

if err := c.ShouldBindUri(&person); err != nil {
    // normal code
    return
}

不同的 HTTP 参数有不同的结构体 tag:

  • 路径参数:            uri
  • 查询字符串参数: form
  • 表单参数:            form
  • HTTP头参数:      header
  • 消息体参数:根据 Content-Type自动选择使用 json 或者 xml;也可以调用 ShouldBindJSON 或者 ShouldBindXML 指定使用那个 tag。

 

针对每种参数类型,Gin 都有对应的函数获取和绑定这些参数。

这些函数都是基于 ShouldBindWith 和 MustBindWith 进行封装:

(1)ShouldBindWith(obj interface{}, b binding.Binding) error

        很多 ShouldBindXXX 函数底层都是调用 ShouldBindWith 函数来完成参数绑定的。

        该函数会根据传入的绑定引擎,将参数绑定到传入的结构体指针中。

        如果绑定失败,只返回错误内容,但不终止 HTTP 请求。

        ShouldBindWith 支持多种绑定引擎,例如:

                binding.JSON、binding.Query、binding.Uri、binging.Header 等。

                更详细的可以参考 binding.go

(2)MustBindWith(obj interface{}, b binding.Binding) error

        很多 BindXXX 函数底层都是调用 MustBindWith 函数来完成参数绑定的。

        该函数会根据传入的绑定引擎,将参数绑定到传入的结构体指针中。

        如果绑定失败,返回错误并终止请求,返回 HTTP 400 错误。

        支持的绑定引擎与 ShouldBindWith 函数一样。

Gin 基于 ShouldBindWith 和 MustBindWith 这两个函数,衍生出很多新的 Bind 函数。

这些函数可以满足不同场景下获取 HTTP 参数的需求。

Gin 提供的函数可以获取 5个类别 的 HTTP 参数。

(1)路径参数

        ShouldBindUri

        BindUri

(2)查询字符串参数

        ShouldBindQuery

        BindQuery

(3)表单参数参数

        ShouldBind

(4)HTTP头参数

        ShouldBindHeader

        BindHeader

(5)消息体参数

        ShouldBindJSON

        BindJSON

注意:Gin 并没有提供类似 ShouldBindForm、BindForm 这类函数来绑定表单参数。

当 HTTP 方法为 GET 时,ShouldBind 只绑定 Query 类型的参数;

当 HTTP 方法为 POST 时,会先检查 Content-Type 是否是 json 或者 xml,如果不是,则绑定 Form 类型的参数。

所以,ShouldBind 可以绑定 Form 类型的参数,但前提是 HTTP 方法是 POST,并且 Content-Type 不是 application/json、application/xml。

建议 使用 ShouldBindXXX,这样可以确保 设置的 HTTP Chain (Chain 可以理解为一个 HTTP 请求的一系列处理插件)能够继续被执行。

5.3 实例

func (u *productHandler) Create (c *gin.Context) {
    u.Lock()
    defer u.Unlock()

    // 1. 参数解析
    var product Product
    if err := c.ShouldBindJSON(&product); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
    }

    // 2. 参数校验
    if _, ok := u.products[product.Name]; ok {
        c.JSON(http.StatusBadRequest, gin.H{
            "error": fmt.Sprintf("product %s already exist", product.Nme)})
        return
    }
    product.CreatedAt = time.Now()

    // 3. 逻辑处理
    u.products[product.Name] = product
    log.Printf("Register product %s success", product.Name)

    // 4. 返回结果
    c.JSON(http.StatusOK, product)
}

Web服务高级功能

1. 中间件

1.1 简介

Gin 支持中间件,HTTP 请求在转发到实际的处理函数,会被一系列加载的中间件进行处理。

1.2 使用

在中间件中,可以解析 HTTP 请求做一些逻辑处理。例如:跨域处理、或者生成 X-Request-ID 保存在 context 中,以便追踪某个请求。

处理完之后,(1)可以选择中断并返回这次请求。(2)也可以将请求继续转交给下一个中间件处理。

当所有的中间件都处理完之后,请求才会转给路由函数进行处理。

1.3 优缺点

通过中间件,可以实现对所有请求都做统一的处理,提高开发效率,并使我们的代码更简洁。

但是,因为所有请求都需要经过中间件的处理,可能会增加请求延时

对于中间件特性,有如下建议

  • 中间件做成可加载的,通过配置文件指定程序启动时加载哪些中间件。
  • 只将一些通用的、必要的功能做成中间件。
  • 在编写中间件时,一定要保证中间件的代码质量性能

1.4 Gin 使用中间件

在 Gin 中,可以通过 gin.Engine 的 Use 方法 来加载中间件。可以加载到不同的位置上,而且不同的位置作用范围也不同。

// 返回一个 gin.Engine
router := gin.New()

// Use 方法加载中间件
// 中间件作用于所有的HTTP请求
router.Use(gin.Logger(), gin.Recovery()) 

// 中间件作用于v1 group
v1 := router.Group("/v1").Use(gin.BasicAuth(gin.Accounts{"foo": "bar"})) 

//中间件只作用于/v1/login API接口
v1.POST("/login", Login).Use(gin.BasicAuth(gin.Accounts{"foo": "bar"})) 

1.5 Gin 支持的中间件

  • gin.Logger()

        Logger 中间件会将日志写到 gin.DefaultWriter

        gin.DefaultWriter 默认为 os.Stdout

  • gin.BasicAuth()

        HTTP 请求基本认证(使用用户名、密码进行认证)

  • gin.Recovery()

        Recovery 中间件可以从任何 panic 恢复,并且写入一个 500 状态码

  • gin.CustomRecovery(handle gin.ReciveryFunc)

        类似 Recover 中间件,但是在恢复时还会调用传入的 handle 方法进行处理。

  • 自定义中间件

        中间件其实是一个函数,函数类型为 gin.HandleFunc

        HandleFunc 底层类型为 func (*Context)

// 自定义 Logger() 中间件
func Logger() gin.HandlerFunc {
    return func (c *gin.Context) {
        t := time.Now()

        // 设置变量 example
        c.Set("example", "12345")

        // 请求之前
        
        c.Next()

        // 请求之后

        latency := time.Since(t)
        log.Print(latency)

        // 访问我们发送的状态
        status := c.Writer.Status()
        log.Println(status)
    }
}

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

    // 自定义的 Logger 中间件
    r.Use(Logger())

    r.GET("/test", func (c *gin.Context) {
        example := c.MustGet("example").(string)

        // print : "12345"
        log.Println(example)
    })

    r.Run(":8080")
}

1.6 开源中间件

中间件功能
gin-jwtJWT 中间件,实现 JWT 认证
gin-swagger自动生成 Swagger 2.0 格式 的 RESTful API 文档
cors实现 HTTP 请求跨域
sessions会话管理中间件
authz基于 casbin 的授权中间件
pprofgin pprof 中间件
go-gin-prometheusPrometheus metrics exporter
gzip支持 HTTP请求 和 响应 的 gzip 压缩
gin-limitHTTP 请求并发控制中间件
requestid给每个 Request 生成 uuid,并添加在返回的 X-Request-ID Header 中。

2. 认证、RequestID、跨域

这三个高级功能,都可以通过 Gin 中间件来实现。

router := gin.New()

// 认证
router.Use(gin.BasicAuth(gin.Accounts{"foo": "bar"}))
router := gin.New()

// RequestID
router.Use(requestid.New(requestid.Config{
    Generator: func() string {
        return "test"
    },
}))
router := gin.New()

// 跨域
// CORS for https://foo.com and https://github.com
// allowing:
//   - PUT and PATCH methods
//   - Origin header
//   - Credentials share
//   - Prefight requests cached for 12 hours
router.Use(cors.New(cors.Config{
    AllowOrigins:     []string{"https://foo.com"}, 
    AllowMethods:     []string{"PUT", "PATCH"},
    AllowHeaders:     []string{"Origin"},
    ExposeHeaders:    []string{"Content-Length"},
    AllowCredentials: true,
    AllowOriginFunc:  func (origin string) bool {
        return origin == "https://github.com"
    },
    MaxAge: 12 * time.Hour,
}))

3. 优雅关停

项目上线后,需要不断迭代来丰富项目功能、修复 Bug 等,这也就意味着,我们要不断的重启服务。

对于 HTTP 服务来说,如果访问量大,重启服务的时候可能还有很多连接没有断开,请求没有完成。如果这时候直接关闭服务,这些连接会直接断掉,请求异常终止,这就会影响到用户体验。

我们期望 HTTP 服务可以在处理完所有请求之后,正常地关闭这些连接,也就是优雅地关闭服务。

有两种方法来优雅关闭 HTTP 服务:

3.1 借助第三方 Go 包

目前使用的比较多的包是 fvbock/endless

可以使用 fvbock/endless 来替换掉 net/http 的 ListenAndServe 方法。

router := gin.Default()

router.GET("/", handler)

...

endless.ListenAndServe(":4242", router)

3.2 自己编码实现 

借助第三方包的好处是可稍微减少一些编码工作量,但缺点是引入了一个新的依赖包。

因此更倾向于自己编码实现。

Go 1.8 版本或者更新的版本,http.Server 内置的 Shutdown 方法,已经实现了优雅关闭。

func main () {
    router := gin.Default()
    router.GET("/", func(c *gin.Context) {
        time.Sleep(5 * time.Second)
        c.String(http.StatusOK, "Welcome Gin Server")
    })

    srv := &http.Server {
        Addr   : ":8080",
        Handler: router,
    }

    // 把 srv.ListenAndServe 放在 goroutine 中执行,
    // 这样才不会阻塞到 srv.Shutdown 函数。
    go func() {
        if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
            log.Fatalf("listen: %s\n", err)
        }
    }()

    // 因为我们把 srv.ListenAndServe 放在了 goroutine 中,
    // 所以需要一种可以让整个进程常驻的机制。
    // 借助 unbuffer channel ,并且调用 signal.Notify 函数将 channel 绑定到 SIGINT、SIGTERM
    // 信号上。这样,收到 SIGINT、SIGTERM 信号后,quit 通道会被写入值,从而结束阻塞状态,程序
    // 继续运行,执行 srv.Shutdown(ctx),优雅关停 HTTP 服务。
    quit := make(chan os.Signal)
    // kill (no param) default send syscall syscall.SIGTERM
    // kill -2  is syscall.SIGINT
    // kill -9  is syscall.SIGKILL but can't be cache, so don't need add it
    signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
    <-quit
    log.Println("Shutting down server...")

    ctx, cancel := context.WithTimeout(context.Background(), 5 * time.Second)
    defer cancel()
    if err := srv.Shutdown(ctx); err != nil {
        log.Fatal("Server forced to shutdown: ", err)
    }
    
    log.Println("Server exiting")
}

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值