区块链技术博客
www.b2bchain.cn

WEB篇·第3章[1]·gin框架学习求职学习资料

本文介绍了WEB篇·第3章[1]·gin框架学习求职学习资料,有助于帮助完成毕业设计以及求职,是一篇很好的资料。

对技术面试,学习经验等有一些体会,在此分享。

[TOC]

1. 认识gin

1.1 官网

https://gin-gonic.com/
https://gin-gonic.com/zh-cn/
Gin 是一个用 Go (Golang) 编写的 HTTP web 框架。 它是一个类似于 martini 但拥有更好性能的 API 框架。

1.2 特性

快速 基于 Radix 树的路由,小内存占用。没有反射。可预测的 API 性能。  支持中间件 传入的 HTTP 请求可以由一系列中间件和最终操作来处理。 例如:Logger,Authorization,GZIP,最终操作 DB。  Crash 处理 Gin 可以 catch 一个发生在 HTTP 请求中的 panic 并 recover 它。这样,你的服务器将始终可用。例如,你可以向 Sentry 报告这个 panic!  JSON 验证 Gin 可以解析并验证请求的 JSON,例如检查所需值的存在。  路由组 更好地组织路由。是否需要授权,不同的 API 版本…… 此外,这些组可以无限制地嵌套而不会降低性能。  错误管理 Gin 提供了一种方便的方法来收集 HTTP 请求期间发生的所有错误。最终,中间件可以将它们写入日志文件,数据库并通过网络发送。  内置渲染 Gin 为 JSON,XML 和 HTML 渲染提供了易于使用的 API。  可扩展性 新建一个中间件非常简单,去查看示例代码吧。

2. 快速入门

2.1 要求

Go版本: 1.13+

2.2 使用

2.2.1 Default引擎

一个最简单的服务示例

package main  // 将gin引入到代码中 import "github.com/gin-gonic/gin"  func main() {         // 得到一个默认的gin-web框架引擎     r := gin.Default()          // 注册路由         // GET:请求方式;/ping:请求的路径     // 当客户端以GET方法请求/ping路径时,会执行后面的匿名函数     r.GET("/ping", func(c *gin.Context) {         c.JSON(200, gin.H{             "message": "pong",         })     })          // 启动引擎     r.Run() // 监听并在 0.0.0.0:8080 上启动服务 }

将上面的代码保存并编译执行,然后使用浏览器打开127.0.0.1:8080/ping就能看到一串JSON字符串{"message": "pong"}
查看源码,我们可以看到默认路由引擎为我们默认使用了 gin自带的日志中间件和crash处理中间件,如下:

func Default() *Engine {     debugPrintWARNINGDefault()     engine := New()     engine.Use(Logger(), Recovery()) //自带的日志中间件和crash处理中间件     return engine }

2.2.2 New引擎

和Default默认路由引擎相比,New只是构造了路由引擎,而没有使用日志中间件和crash处理中间件,这意味着使用者需要自行构造自己的日志处理中间和crash处理机制,当然,再通过Use注册中间件来达到自己的业务需求也是可以的。
简单测试New引擎的crash异常示例:

package main  // 将gin引入到代码中 import "github.com/gin-gonic/gin"  func main() {         // 仅仅是New一个gin引擎(无Logger、 Recovery中间件)     r := gin.New()          // 注册路由         // GET:请求方式;/ping:请求的路径     // 当客户端以GET方法请求/ping路径时,会执行后面的匿名函数     r.GET("/ping", func(c *gin.Context) {                 panic("测试New没有crash处理机制的后果是程序进程直接退出!")         c.JSON(200, gin.H{             "message": "pong",         })     })          // 启动引擎     r.Run() // 监听并在 0.0.0.0:8080 上启动服务 }

3. Restful简介

REST 是 Representational State Transfer的缩写,如果一个架构符合REST原则,就称它为RESTful架构。
RESTful 架构可以充分的利用 HTTP 协议的各种功能,是 HTTP 协议的最佳实践。
RESTful API 是一种软件架构风格、设计风格,可以让软件更加清晰,更简洁,更有层次,可维护性更好。
更详细的介绍可参考
https://restfulapi.cn/http://www.ruanyifeng.com/blog/2011/09/restful.html

3.1 API请求设计

WEB篇·第3章[1]·gin框架学习

3.2 API响应设计

WEB篇·第3章[1]·gin框架学习
WEB篇·第3章[1]·gin框架学习

4. gin路由

4.1 普通路由

r.GET("/index", func(c *gin.Context) {...}) r.GET("/login", func(c *gin.Context) {...}) r.POST("/login", func(c *gin.Context) {...})

4.1.1 Any路由

匹配所有请求方法的Any方法(不关心客户端请求方式的一种兼容性处理?):

r.Any("/any", func(c *gin.Context) {     c.JSON(http.StatusOK, gin.H{     "msg": "match any route handler",     }) })

WEB篇·第3章[1]·gin框架学习

4.2 路由组

我们可以将拥有共同URL前缀的路由划分为一个路由组。习惯性一对{}包裹同组的路由,这只是为了看着清晰,你用不用{}包裹功能上没什么区别。
路由组也是支持无限嵌套而不损失性能。

accountGrp := r.Group("/account") {     accountGrp.GET("/list", accountList)     accountGrp.POST("/new", accountAdd)     accountGrp.GET("/:id/:name", acntShouldBindingURI)     accountGrp.GET("/:id", accountParam)     accountGrp.GET("/query", accountDefaultQuery)     accountGrp.POST("/json", accountBindJson) }  fileGrp := r.Group("/file") {     fileGrp.GET("/*xxx", fileAbsURL) }

路由原理
Gin框架中的路由使用的是httprouter这个库。其基本原理就是构造一个路由地址的前缀树。

4.3 路由拆分

4.3.1 拆分到多个路由文件,例如:

gin_demo ├── go.mod ├── go.sum ├── main.go └── routers     ├── blog.go     └── shop.go
routers/shop.go中添加一个LoadShop的函数,将shop相关的路由注册到指定的路由器: func LoadShop(e *gin.Engine)  {     e.GET("/hello", helloHandler)     e.GET("/goods", goodsHandler)     e.GET("/checkout", checkoutHandler)     ... }  routers/blog.go中添加一个`LoadBlog的函数,将blog相关的路由注册到指定的路由器: func LoadBlog(e *gin.Engine) {     e.GET("/post", postHandler)     e.GET("/comment", commentHandler)     ... }  在main函数中实现最终的注册逻辑如下: func main() {     r := gin.Default()     routers.LoadBlog(r)     routers.LoadShop(r)     if err := r.Run(); err != nil {         fmt.Println("startup service failed, err:%vn", err)     } }

4.3.2 拆分到不同APP

gin_demo ├── app │   ├── blog │   │   ├── handler.go │   │   └── router.go │   └── shop │       ├── handler.go │       └── router.go ├── go.mod ├── go.sum ├── main.go └── routers     └── routers.go
其中app/blog/router.go用来定义blog相关的路由信息,具体内容如下: func Routers(e *gin.Engine) {     e.GET("/post", postHandler)     e.GET("/comment", commentHandler) }  app/shop/router.go用来定义shop相关路由信息,具体内容如下: func Routers(e *gin.Engine) {     e.GET("/goods", goodsHandler)     e.GET("/checkout", checkoutHandler) }  routers/routers.go中根据需要定义Include函数用来注册子app中定义的路由,Init函数用来进行路由的初始化操作: type Option func(*gin.Engine) var options = []Option{} // 注册app的路由配置 func Include(opts ...Option) {     options = append(options, opts...) } // 初始化 func Init() *gin.Engine {     r := gin.Default()     for _, opt := range options {         opt(r)     }     return r }  main.go中按如下方式先注册子app中的路由,然后再进行路由的初始化: func main() {     // 加载多个APP的路由配置     routers.Include(shop.Routers, blog.Routers)     // 初始化路由     r := routers.Init()     if err := r.Run(); err != nil {         fmt.Println("startup service failed, err:%vn", err)     } }

5. 中间件

类似java中的面向切面(AOP),Gin框架允许开发者在处理请求的过程中,加入用户自己的钩子(Hook)函数。这个钩子函数就叫中间件。
中间件适合处理一些公共的业务逻辑,比如登录认证、权限校验、数据分页、记录日志、耗时统计等。

5.1 定义中间件

// HandlerFunc defines the handler used by gin middleware as return value.
type HandlerFunc func(*Context)
WEB篇·第3章[1]·gin框架学习

5.2 注册中间件

在gin框架中,我们可以为每个路由添加任意数量的中间件。

5.2.1 注册全局中间件

// 方式1 闭包返回匿名gin.HandlerFunc函数即 func(c *gin.Context) // Cost 一个统计耗时请求耗时的中间件 func Cost() gin.HandlerFunc {     return func(c *gin.Context) {         start := time.Now()          // 调用该请求的剩余处理程序         c.Next()          // 不调用该请求的剩余处理程序         // c.Abort()          // 计算耗时         cost := time.Since(start)         fmt.Println("Since", cost)          cost = time.Now().Sub(start) //可更精确的体现[推荐]         fmt.Println("Sub", cost)     } }  r.Use(Cost())  // 方式2 func Cost2(c *gin.Context)  {     start := time.Now()      // 调用该请求的剩余处理程序     c.Next()      // 不调用该请求的剩余处理程序     // c.Abort()      // 计算耗时     cost := time.Since(start)     fmt.Println("Since", cost)      cost = time.Now().Sub(start)     fmt.Println("Sub", cost) }  r.Uset(Cost2)

5.2.2 为某个路由单独注册

(relativePath string, handlers ...HandlerFunc)

// 给路由单独注册中间件(可注册多个) r.Any("/any", Cost2, func(c *gin.Context) {     c.JSON(http.StatusOK, gin.H{     "msg": "match any route handler", })

5.2.3 为路由组注册中间件

Group(relativePath string, handlers ...HandlerFunc)
为路由组注册中间件有以下两种写法。

// 写法1: shopGroup := r.Group("/shop", Cost()) {     shopGroup.GET("/index", func(c *gin.Context) {...})     ... }  // 写法2: shopGroup := r.Group("/shop") shopGroup.Use(Cost()) {     shopGroup.GET("/index", func(c *gin.Context) {...})     ... }

注意 gin中间件中使用goroutine

当在中间件或handler中启动新的goroutine时,不能使用原始的上下文(c *gin.Context),必须使用其只读副本(c.Copy())。

5.3 Abort退出中间件

通过 c.Abort()

func LoginChk(c *gin.Context) {     name, ok := c.Get("name")     if !ok {         c.Abort()     }     ...     c.Next()     ... }
本质 :让c.Next()中的循环立即结束 const abortIndex int8 = math.MaxInt8 / 2  func (c *Context) Abort() {     c.index = abortIndex }  func (c *Context) Next() {     c.index++     for c.index < int8(len(c.handlers)) {         c.handlers[c.index](c)         c.index++     } }

6. 解析请求参数

6.1 GET – Path参数

/:id/:name

fileAbsURL := c.Param("xxx")  id := c.Param("id") name := c.Param("name")

6.2 GET – Query参数

?query=val1&param=val2

id := c.DefaultQuery("id", "0") // 如果取不到值返回给定的默认值; name := c.Query("name") // 如果取不到值返回空串 ""(具体可查看源码解释)

6.3 参数绑定

为了能够更方便的获取请求相关参数,提高开发效率,我们可以基于请求的Content-Type识别请求数据类型并利用反射机制自动提取请求中QueryString、form表单、JSON、XML等参数到结构体中。 下面的示例代码演示了.ShouldBind()强大的功能,它能够基于请求自动提取JSON、form表单和QueryString类型的数据,并把值绑定到指定的结构体对象。

5.3.1 GET – uri参数绑定 – ShouldBindUri

格式 uri:"key" binding:"required"

type Account struct {     No   int    `uri:"id" binding:"required"`     Name string `uri:"name" binding:"required"` } account := new(model.Account) err := c.ShouldBindUri(account)

6.3.2 POST – query/from/json/xml等 – ShouldBind

格式 uri:"urikey" form:"formkey" json:"jsonkey" binding:"required"
// ShouldBind()会根据请求的Content-Type自行选择绑定器
WEB篇·第3章[1]·gin框架学习
WEB篇·第3章[1]·gin框架学习

type Login struct {     User     string `uri:"user" form:"user" json:"user" binding:"required"`     Password string `uri:"pwd" form:"pwd" json:"pass" binding:"required"` }  var login Login // 1) 绑定QueryString示例 (/loginQuery?user=q1mi&password=123456) //router.GET("/loginquery",  // 2) 绑定JSON的示例 ({"user": "q1mi", "password": "123456"}) // router.POST("/loginjson",  // 绑定form表单示例 (user=q1mi password=123456) // router.POST("/loginform"  // ShouldBind()会根据请求的Content-Type自行选择绑定器 if err := c.ShouldBind(&login); err == nil {     fmt.Printf("login info:%#vn", login)     c.JSON(http.StatusOK, gin.H{         "user":     login.User,         "password": login.Password,     }) } else {     c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) }

7. gin服务优雅退出

7.1 优雅退出

优雅退出就是服务端退出命令发出后不是立即退出,而是等待当前还在处理的请求全部处理完毕后再退出程序,是一种对客户端友好的退出方式。而执行Ctrl+C关闭服务端时,会强制结束进程导致正在访问的请求出现问题。
Go 1.8版本之后, http.Server 内置的 Shutdown() 方法就支持优雅地关机,具体示例如下

package main  import (     "context"     "log"     "net/http"     "os"     "os/signal"     "syscall"     "time"      "github.com/gin-gonic/gin" )  func main() {     r := gin.Default()      r.GET("/notify/signal/shutdown", func(c *gin.Context) {         time.Sleep(5 * time.Second)         c.String(http.StatusOK, "Test notify signal to shutdown server !")     })      svr := http.Server{         Addr:    ":8080",         Handler: r,     }      go func() {         // 开启一个goroutine启动服务         if err := svr.ListenAndServe(); err != nil && err != http.ErrServerClosed {             log.Fatalf("listen: %sn", err)         }     }()      // 等待中断信号来优雅地关闭服务器,为关闭服务器操作设置一个5秒的超时     quit := make(chan os.Signal, 1) // 创建一个接收信号的通道      // kill 默认会发送 syscall.SIGTERM 信号     // kill -2 发送 syscall.SIGINT 信号,我们常用的Ctrl+C就是触发系统SIGINT信号     // kill -9 发送 syscall.SIGKILL 信号,但是不能被捕获,所以不需要添加它     // signal.Notify把收到的 syscall.SIGINT或syscall.SIGTERM 信号转发给quit     signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) // 此处不会阻塞     <-quit                                               // 阻塞在此,当接收到上述两种信号时才会往下执行      log.Println("Shutdown Server ...")     // 创建一个10秒超时的context     ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)     defer cancel()      // 5秒内优雅关闭服务(将未处理完的请求处理完再关闭服务),超过5秒就超时退出     if err := svr.Shutdown(ctx); err != nil {         log.Fatal("Server Shutdown: ", err)     }      log.Println("Server exited") }

7.2 优雅重启

优雅关机实现了,那么该如何实现优雅重启呢?
我们可以使用 fvbock/endless 来替换默认的 ListenAndServe启动服务来实现,
示例代码如下:

7.2.1 兼容windows

windows下的信号没有 SIGUSR1、SIGUSR2 等,为了不轻易的抛弃 windows 环境 (不想折腾 mac、ubuntu)。目前用了一个 dirty 的办法解决:
在 go 的安装目录修改 Gosrcsyscalltypes_windows.go,增加如下代码:

var signals = [...]string{     // 这里省略N行。。。。      /** 兼容windows start */     16: "SIGUSR1",     17: "SIGUSR2",     18: "SIGTSTP",     /** 兼容windows end */ }  /** 兼容windows start */ func Kill(...interface{}) {     return; } const (     SIGUSR1 = Signal(16)     SIGUSR2 = Signal(17)     SIGTSTP = Signal(18) ) /** 兼容windows end */
package main  import (     "log"     "net/http"     "time"      "github.com/fvbock/endless"     "github.com/gin-gonic/gin" )  func main() {     router := gin.Default()     router.GET("/", func(c *gin.Context) {         time.Sleep(15 * time.Second)         c.String(http.StatusOK, "hello gin!")     })      // 默认endless服务器会监听下列信号:     // syscall.SIGHUP,syscall.SIGUSR1,syscall.SIGUSR2,syscall.SIGINT,syscall.SIGTERM和syscall.SIGTSTP     // 接收到 SIGHUP 信号将触发`fork/restart` 实现优雅重启(kill -1 pid会发送SIGHUP信号)     // 接收到 syscall.SIGINT或syscall.SIGTERM 信号将触发优雅关机     // 接收到 SIGUSR2 信号将触发HammerTime     // SIGUSR1 和 SIGTSTP 被用来触发一些用户自定义的hook函数      if err := endless.ListenAndServe(":8080", router); err != nil {         log.Fatalf("listen: %sn", err)     }      log.Println("Server exiting") }

如何验证优雅重启的效果呢?
我们通过执行kill -1 pid命令发送syscall.SIGINT来通知程序优雅重启,具体做法如下:
打开终端,go build -o graceful_restart编译并执行./graceful_restart,终端输出当前pid(假设为43682)
将代码中处理请求函数返回的hello gin!修改为hello happy!,再次编译go build -o graceful_restart
打开一个浏览器,访问127.0.0.1:8080/,此时浏览器白屏等待服务端返回响应。
在终端迅速执行kill -1 43682命令给程序发送syscall.SIGHUP信号
等第3步浏览器收到响应信息hello gin!后再次访问127.0.0.1:8080/会收到hello q1mi!的响应。
在不影响当前未处理完请求的同时完成了程序代码的替换,实现了优雅重启。
但是需要注意的是,此时程序的PID变化了,因为endless 是通过fork子进程处理新请求,待原进程处理完当前请求后再退出的方式实现优雅重启的。所以当你的项目是使用类似supervisor的软件管理进程时就不适用这种方式了。

总结
无论是优雅关机还是优雅重启归根结底都是通过监听特定系统信号,然后执行一定的逻辑处理保障当前系统正在处理的请求被正常处理后再关闭当前进程。使用优雅关机还是使用优雅重启以及怎么实现,这就需要根据项目实际情况来决定了。

[TOC]

1. 认识gin

1.1 官网

https://gin-gonic.com/
https://gin-gonic.com/zh-cn/
Gin 是一个用 Go (Golang) 编写的 HTTP web 框架。 它是一个类似于 martini 但拥有更好性能的 API 框架。

1.2 特性

快速 基于 Radix 树的路由,小内存占用。没有反射。可预测的 API 性能。  支持中间件 传入的 HTTP 请求可以由一系列中间件和最终操作来处理。 例如:Logger,Authorization,GZIP,最终操作 DB。  Crash 处理 Gin 可以 catch 一个发生在 HTTP 请求中的 panic 并 recover 它。这样,你的服务器将始终可用。例如,你可以向 Sentry 报告这个 panic!  JSON 验证 Gin 可以解析并验证请求的 JSON,例如检查所需值的存在。  路由组 更好地组织路由。是否需要授权,不同的 API 版本…… 此外,这些组可以无限制地嵌套而不会降低性能。  错误管理 Gin 提供了一种方便的方法来收集 HTTP 请求期间发生的所有错误。最终,中间件可以将它们写入日志文件,数据库并通过网络发送。  内置渲染 Gin 为 JSON,XML 和 HTML 渲染提供了易于使用的 API。  可扩展性 新建一个中间件非常简单,去查看示例代码吧。

2. 快速入门

2.1 要求

Go版本: 1.13+

2.2 使用

2.2.1 Default引擎

一个最简单的服务示例

package main  // 将gin引入到代码中 import "github.com/gin-gonic/gin"  func main() {         // 得到一个默认的gin-web框架引擎     r := gin.Default()          // 注册路由         // GET:请求方式;/ping:请求的路径     // 当客户端以GET方法请求/ping路径时,会执行后面的匿名函数     r.GET("/ping", func(c *gin.Context) {         c.JSON(200, gin.H{             "message": "pong",         })     })          // 启动引擎     r.Run() // 监听并在 0.0.0.0:8080 上启动服务 }

将上面的代码保存并编译执行,然后使用浏览器打开127.0.0.1:8080/ping就能看到一串JSON字符串{"message": "pong"}
查看源码,我们可以看到默认路由引擎为我们默认使用了 gin自带的日志中间件和crash处理中间件,如下:

func Default() *Engine {     debugPrintWARNINGDefault()     engine := New()     engine.Use(Logger(), Recovery()) //自带的日志中间件和crash处理中间件     return engine }

2.2.2 New引擎

和Default默认路由引擎相比,New只是构造了路由引擎,而没有使用日志中间件和crash处理中间件,这意味着使用者需要自行构造自己的日志处理中间和crash处理机制,当然,再通过Use注册中间件来达到自己的业务需求也是可以的。
简单测试New引擎的crash异常示例:

package main  // 将gin引入到代码中 import "github.com/gin-gonic/gin"  func main() {         // 仅仅是New一个gin引擎(无Logger、 Recovery中间件)     r := gin.New()          // 注册路由         // GET:请求方式;/ping:请求的路径     // 当客户端以GET方法请求/ping路径时,会执行后面的匿名函数     r.GET("/ping", func(c *gin.Context) {                 panic("测试New没有crash处理机制的后果是程序进程直接退出!")         c.JSON(200, gin.H{             "message": "pong",         })     })          // 启动引擎     r.Run() // 监听并在 0.0.0.0:8080 上启动服务 }

3. Restful简介

REST 是 Representational State Transfer的缩写,如果一个架构符合REST原则,就称它为RESTful架构。
RESTful 架构可以充分的利用 HTTP 协议的各种功能,是 HTTP 协议的最佳实践。
RESTful API 是一种软件架构风格、设计风格,可以让软件更加清晰,更简洁,更有层次,可维护性更好。
更详细的介绍可参考
https://restfulapi.cn/http://www.ruanyifeng.com/blog/2011/09/restful.html

3.1 API请求设计

WEB篇·第3章[1]·gin框架学习

3.2 API响应设计

WEB篇·第3章[1]·gin框架学习
WEB篇·第3章[1]·gin框架学习

4. gin路由

4.1 普通路由

r.GET("/index", func(c *gin.Context) {...}) r.GET("/login", func(c *gin.Context) {...}) r.POST("/login", func(c *gin.Context) {...})

4.1.1 Any路由

匹配所有请求方法的Any方法(不关心客户端请求方式的一种兼容性处理?):

r.Any("/any", func(c *gin.Context) {     c.JSON(http.StatusOK, gin.H{     "msg": "match any route handler",     }) })

WEB篇·第3章[1]·gin框架学习

4.2 路由组

我们可以将拥有共同URL前缀的路由划分为一个路由组。习惯性一对{}包裹同组的路由,这只是为了看着清晰,你用不用{}包裹功能上没什么区别。
路由组也是支持无限嵌套而不损失性能。

accountGrp := r.Group("/account") {     accountGrp.GET("/list", accountList)     accountGrp.POST("/new", accountAdd)     accountGrp.GET("/:id/:name", acntShouldBindingURI)     accountGrp.GET("/:id", accountParam)     accountGrp.GET("/query", accountDefaultQuery)     accountGrp.POST("/json", accountBindJson) }  fileGrp := r.Group("/file") {     fileGrp.GET("/*xxx", fileAbsURL) }

路由原理
Gin框架中的路由使用的是httprouter这个库。其基本原理就是构造一个路由地址的前缀树。

4.3 路由拆分

4.3.1 拆分到多个路由文件,例如:

gin_demo ├── go.mod ├── go.sum ├── main.go └── routers     ├── blog.go     └── shop.go
routers/shop.go中添加一个LoadShop的函数,将shop相关的路由注册到指定的路由器: func LoadShop(e *gin.Engine)  {     e.GET("/hello", helloHandler)     e.GET("/goods", goodsHandler)     e.GET("/checkout", checkoutHandler)     ... }  routers/blog.go中添加一个`LoadBlog的函数,将blog相关的路由注册到指定的路由器: func LoadBlog(e *gin.Engine) {     e.GET("/post", postHandler)     e.GET("/comment", commentHandler)     ... }  在main函数中实现最终的注册逻辑如下: func main() {     r := gin.Default()     routers.LoadBlog(r)     routers.LoadShop(r)     if err := r.Run(); err != nil {         fmt.Println("startup service failed, err:%vn", err)     } }

4.3.2 拆分到不同APP

gin_demo ├── app │   ├── blog │   │   ├── handler.go │   │   └── router.go │   └── shop │       ├── handler.go │       └── router.go ├── go.mod ├── go.sum ├── main.go └── routers     └── routers.go
其中app/blog/router.go用来定义blog相关的路由信息,具体内容如下: func Routers(e *gin.Engine) {     e.GET("/post", postHandler)     e.GET("/comment", commentHandler) }  app/shop/router.go用来定义shop相关路由信息,具体内容如下: func Routers(e *gin.Engine) {     e.GET("/goods", goodsHandler)     e.GET("/checkout", checkoutHandler) }  routers/routers.go中根据需要定义Include函数用来注册子app中定义的路由,Init函数用来进行路由的初始化操作: type Option func(*gin.Engine) var options = []Option{} // 注册app的路由配置 func Include(opts ...Option) {     options = append(options, opts...) } // 初始化 func Init() *gin.Engine {     r := gin.Default()     for _, opt := range options {         opt(r)     }     return r }  main.go中按如下方式先注册子app中的路由,然后再进行路由的初始化: func main() {     // 加载多个APP的路由配置     routers.Include(shop.Routers, blog.Routers)     // 初始化路由     r := routers.Init()     if err := r.Run(); err != nil {         fmt.Println("startup service failed, err:%vn", err)     } }

5. 中间件

类似java中的面向切面(AOP),Gin框架允许开发者在处理请求的过程中,加入用户自己的钩子(Hook)函数。这个钩子函数就叫中间件。
中间件适合处理一些公共的业务逻辑,比如登录认证、权限校验、数据分页、记录日志、耗时统计等。

5.1 定义中间件

// HandlerFunc defines the handler used by gin middleware as return value.
type HandlerFunc func(*Context)
WEB篇·第3章[1]·gin框架学习

5.2 注册中间件

在gin框架中,我们可以为每个路由添加任意数量的中间件。

5.2.1 注册全局中间件

// 方式1 闭包返回匿名gin.HandlerFunc函数即 func(c *gin.Context) // Cost 一个统计耗时请求耗时的中间件 func Cost() gin.HandlerFunc {     return func(c *gin.Context) {         start := time.Now()          // 调用该请求的剩余处理程序         c.Next()          // 不调用该请求的剩余处理程序         // c.Abort()          // 计算耗时         cost := time.Since(start)         fmt.Println("Since", cost)          cost = time.Now().Sub(start) //可更精确的体现[推荐]         fmt.Println("Sub", cost)     } }  r.Use(Cost())  // 方式2 func Cost2(c *gin.Context)  {     start := time.Now()      // 调用该请求的剩余处理程序     c.Next()      // 不调用该请求的剩余处理程序     // c.Abort()      // 计算耗时     cost := time.Since(start)     fmt.Println("Since", cost)      cost = time.Now().Sub(start)     fmt.Println("Sub", cost) }  r.Uset(Cost2)

5.2.2 为某个路由单独注册

(relativePath string, handlers ...HandlerFunc)

// 给路由单独注册中间件(可注册多个) r.Any("/any", Cost2, func(c *gin.Context) {     c.JSON(http.StatusOK, gin.H{     "msg": "match any route handler", })

5.2.3 为路由组注册中间件

Group(relativePath string, handlers ...HandlerFunc)
为路由组注册中间件有以下两种写法。

// 写法1: shopGroup := r.Group("/shop", Cost()) {     shopGroup.GET("/index", func(c *gin.Context) {...})     ... }  // 写法2: shopGroup := r.Group("/shop") shopGroup.Use(Cost()) {     shopGroup.GET("/index", func(c *gin.Context) {...})     ... }

注意 gin中间件中使用goroutine

当在中间件或handler中启动新的goroutine时,不能使用原始的上下文(c *gin.Context),必须使用其只读副本(c.Copy())。

5.3 Abort退出中间件

通过 c.Abort()

func LoginChk(c *gin.Context) {     name, ok := c.Get("name")     if !ok {         c.Abort()     }     ...     c.Next()     ... }
本质 :让c.Next()中的循环立即结束 const abortIndex int8 = math.MaxInt8 / 2  func (c *Context) Abort() {     c.index = abortIndex }  func (c *Context) Next() {     c.index++     for c.index < int8(len(c.handlers)) {         c.handlers[c.index](c)         c.index++     } }

6. 解析请求参数

6.1 GET – Path参数

/:id/:name

fileAbsURL := c.Param("xxx")  id := c.Param("id") name := c.Param("name")

6.2 GET – Query参数

?query=val1&param=val2

id := c.DefaultQuery("id", "0") // 如果取不到值返回给定的默认值; name := c.Query("name") // 如果取不到值返回空串 ""(具体可查看源码解释)

6.3 参数绑定

为了能够更方便的获取请求相关参数,提高开发效率,我们可以基于请求的Content-Type识别请求数据类型并利用反射机制自动提取请求中QueryString、form表单、JSON、XML等参数到结构体中。 下面的示例代码演示了.ShouldBind()强大的功能,它能够基于请求自动提取JSON、form表单和QueryString类型的数据,并把值绑定到指定的结构体对象。

5.3.1 GET – uri参数绑定 – ShouldBindUri

格式 uri:"key" binding:"required"

type Account struct {     No   int    `uri:"id" binding:"required"`     Name string `uri:"name" binding:"required"` } account := new(model.Account) err := c.ShouldBindUri(account)

6.3.2 POST – query/from/json/xml等 – ShouldBind

格式 uri:"urikey" form:"formkey" json:"jsonkey" binding:"required"
// ShouldBind()会根据请求的Content-Type自行选择绑定器
WEB篇·第3章[1]·gin框架学习
WEB篇·第3章[1]·gin框架学习

type Login struct {     User     string `uri:"user" form:"user" json:"user" binding:"required"`     Password string `uri:"pwd" form:"pwd" json:"pass" binding:"required"` }  var login Login // 1) 绑定QueryString示例 (/loginQuery?user=q1mi&password=123456) //router.GET("/loginquery",  // 2) 绑定JSON的示例 ({"user": "q1mi", "password": "123456"}) // router.POST("/loginjson",  // 绑定form表单示例 (user=q1mi password=123456) // router.POST("/loginform"  // ShouldBind()会根据请求的Content-Type自行选择绑定器 if err := c.ShouldBind(&login); err == nil {     fmt.Printf("login info:%#vn", login)     c.JSON(http.StatusOK, gin.H{         "user":     login.User,         "password": login.Password,     }) } else {     c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) }

7. gin服务优雅退出

7.1 优雅退出

优雅退出就是服务端退出命令发出后不是立即退出,而是等待当前还在处理的请求全部处理完毕后再退出程序,是一种对客户端友好的退出方式。而执行Ctrl+C关闭服务端时,会强制结束进程导致正在访问的请求出现问题。
Go 1.8版本之后, http.Server 内置的 Shutdown() 方法就支持优雅地关机,具体示例如下

package main  import (     "context"     "log"     "net/http"     "os"     "os/signal"     "syscall"     "time"      "github.com/gin-gonic/gin" )  func main() {     r := gin.Default()      r.GET("/notify/signal/shutdown", func(c *gin.Context) {         time.Sleep(5 * time.Second)         c.String(http.StatusOK, "Test notify signal to shutdown server !")     })      svr := http.Server{         Addr:    ":8080",         Handler: r,     }      go func() {         // 开启一个goroutine启动服务         if err := svr.ListenAndServe(); err != nil && err != http.ErrServerClosed {             log.Fatalf("listen: %sn", err)         }     }()      // 等待中断信号来优雅地关闭服务器,为关闭服务器操作设置一个5秒的超时     quit := make(chan os.Signal, 1) // 创建一个接收信号的通道      // kill 默认会发送 syscall.SIGTERM 信号     // kill -2 发送 syscall.SIGINT 信号,我们常用的Ctrl+C就是触发系统SIGINT信号     // kill -9 发送 syscall.SIGKILL 信号,但是不能被捕获,所以不需要添加它     // signal.Notify把收到的 syscall.SIGINT或syscall.SIGTERM 信号转发给quit     signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) // 此处不会阻塞     <-quit                                               // 阻塞在此,当接收到上述两种信号时才会往下执行      log.Println("Shutdown Server ...")     // 创建一个10秒超时的context     ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)     defer cancel()      // 5秒内优雅关闭服务(将未处理完的请求处理完再关闭服务),超过5秒就超时退出     if err := svr.Shutdown(ctx); err != nil {         log.Fatal("Server Shutdown: ", err)     }      log.Println("Server exited") }

7.2 优雅重启

优雅关机实现了,那么该如何实现优雅重启呢?
我们可以使用 fvbock/endless 来替换默认的 ListenAndServe启动服务来实现,
示例代码如下:

7.2.1 兼容windows

windows下的信号没有 SIGUSR1、SIGUSR2 等,为了不轻易的抛弃 windows 环境 (不想折腾 mac、ubuntu)。目前用了一个 dirty 的办法解决:
在 go 的安装目录修改 Gosrcsyscalltypes_windows.go,增加如下代码:

var signals = [...]string{     // 这里省略N行。。。。      /** 兼容windows start */     16: "SIGUSR1",     17: "SIGUSR2",     18: "SIGTSTP",     /** 兼容windows end */ }  /** 兼容windows start */ func Kill(...interface{}) {     return; } const (     SIGUSR1 = Signal(16)     SIGUSR2 = Signal(17)     SIGTSTP = Signal(18) ) /** 兼容windows end */
package main  import (     "log"     "net/http"     "time"      "github.com/fvbock/endless"     "github.com/gin-gonic/gin" )  func main() {     router := gin.Default()     router.GET("/", func(c *gin.Context) {         time.Sleep(15 * time.Second)         c.String(http.StatusOK, "hello gin!")     })      // 默认endless服务器会监听下列信号:     // syscall.SIGHUP,syscall.SIGUSR1,syscall.SIGUSR2,syscall.SIGINT,syscall.SIGTERM和syscall.SIGTSTP     // 接收到 SIGHUP 信号将触发`fork/restart` 实现优雅重启(kill -1 pid会发送SIGHUP信号)     // 接收到 syscall.SIGINT或syscall.SIGTERM 信号将触发优雅关机     // 接收到 SIGUSR2 信号将触发HammerTime     // SIGUSR1 和 SIGTSTP 被用来触发一些用户自定义的hook函数      if err := endless.ListenAndServe(":8080", router); err != nil {         log.Fatalf("listen: %sn", err)     }      log.Println("Server exiting") }

如何验证优雅重启的效果呢?
我们通过执行kill -1 pid命令发送syscall.SIGINT来通知程序优雅重启,具体做法如下:
打开终端,go build -o graceful_restart编译并执行./graceful_restart,终端输出当前pid(假设为43682)
将代码中处理请求函数返回的hello gin!修改为hello happy!,再次编译go build -o graceful_restart
打开一个浏览器,访问127.0.0.1:8080/,此时浏览器白屏等待服务端返回响应。
在终端迅速执行kill -1 43682命令给程序发送syscall.SIGHUP信号
等第3步浏览器收到响应信息hello gin!后再次访问127.0.0.1:8080/会收到hello q1mi!的响应。
在不影响当前未处理完请求的同时完成了程序代码的替换,实现了优雅重启。
但是需要注意的是,此时程序的PID变化了,因为endless 是通过fork子进程处理新请求,待原进程处理完当前请求后再退出的方式实现优雅重启的。所以当你的项目是使用类似supervisor的软件管理进程时就不适用这种方式了。

总结
无论是优雅关机还是优雅重启归根结底都是通过监听特定系统信号,然后执行一定的逻辑处理保障当前系统正在处理的请求被正常处理后再关闭当前进程。使用优雅关机还是使用优雅重启以及怎么实现,这就需要根据项目实际情况来决定了。

[TOC]

1. 认识gin

1.1 官网

https://gin-gonic.com/
https://gin-gonic.com/zh-cn/
Gin 是一个用 Go (Golang) 编写的 HTTP web 框架。 它是一个类似于 martini 但拥有更好性能的 API 框架。

1.2 特性

快速 基于 Radix 树的路由,小内存占用。没有反射。可预测的 API 性能。  支持中间件 传入的 HTTP 请求可以由一系列中间件和最终操作来处理。 例如:Logger,Authorization,GZIP,最终操作 DB。  Crash 处理 Gin 可以 catch 一个发生在 HTTP 请求中的 panic 并 recover 它。这样,你的服务器将始终可用。例如,你可以向 Sentry 报告这个 panic!  JSON 验证 Gin 可以解析并验证请求的 JSON,例如检查所需值的存在。  路由组 更好地组织路由。是否需要授权,不同的 API 版本…… 此外,这些组可以无限制地嵌套而不会降低性能。  错误管理 Gin 提供了一种方便的方法来收集 HTTP 请求期间发生的所有错误。最终,中间件可以将它们写入日志文件,数据库并通过网络发送。  内置渲染 Gin 为 JSON,XML 和 HTML 渲染提供了易于使用的 API。  可扩展性 新建一个中间件非常简单,去查看示例代码吧。

2. 快速入门

2.1 要求

Go版本: 1.13+

2.2 使用

2.2.1 Default引擎

一个最简单的服务示例

package main  // 将gin引入到代码中 import "github.com/gin-gonic/gin"  func main() {         // 得到一个默认的gin-web框架引擎     r := gin.Default()          // 注册路由         // GET:请求方式;/ping:请求的路径     // 当客户端以GET方法请求/ping路径时,会执行后面的匿名函数     r.GET("/ping", func(c *gin.Context) {         c.JSON(200, gin.H{             "message": "pong",         })     })          // 启动引擎     r.Run() // 监听并在 0.0.0.0:8080 上启动服务 }

将上面的代码保存并编译执行,然后使用浏览器打开127.0.0.1:8080/ping就能看到一串JSON字符串{"message": "pong"}
查看源码,我们可以看到默认路由引擎为我们默认使用了 gin自带的日志中间件和crash处理中间件,如下:

func Default() *Engine {     debugPrintWARNINGDefault()     engine := New()     engine.Use(Logger(), Recovery()) //自带的日志中间件和crash处理中间件     return engine }

2.2.2 New引擎

和Default默认路由引擎相比,New只是构造了路由引擎,而没有使用日志中间件和crash处理中间件,这意味着使用者需要自行构造自己的日志处理中间和crash处理机制,当然,再通过Use注册中间件来达到自己的业务需求也是可以的。
简单测试New引擎的crash异常示例:

package main  // 将gin引入到代码中 import "github.com/gin-gonic/gin"  func main() {         // 仅仅是New一个gin引擎(无Logger、 Recovery中间件)     r := gin.New()          // 注册路由         // GET:请求方式;/ping:请求的路径     // 当客户端以GET方法请求/ping路径时,会执行后面的匿名函数     r.GET("/ping", func(c *gin.Context) {                 panic("测试New没有crash处理机制的后果是程序进程直接退出!")         c.JSON(200, gin.H{             "message": "pong",         })     })          // 启动引擎     r.Run() // 监听并在 0.0.0.0:8080 上启动服务 }

3. Restful简介

REST 是 Representational State Transfer的缩写,如果一个架构符合REST原则,就称它为RESTful架构。
RESTful 架构可以充分的利用 HTTP 协议的各种功能,是 HTTP 协议的最佳实践。
RESTful API 是一种软件架构风格、设计风格,可以让软件更加清晰,更简洁,更有层次,可维护性更好。
更详细的介绍可参考
https://restfulapi.cn/http://www.ruanyifeng.com/blog/2011/09/restful.html

3.1 API请求设计

WEB篇·第3章[1]·gin框架学习

3.2 API响应设计

WEB篇·第3章[1]·gin框架学习
WEB篇·第3章[1]·gin框架学习

4. gin路由

4.1 普通路由

r.GET("/index", func(c *gin.Context) {...}) r.GET("/login", func(c *gin.Context) {...}) r.POST("/login", func(c *gin.Context) {...})

4.1.1 Any路由

匹配所有请求方法的Any方法(不关心客户端请求方式的一种兼容性处理?):

r.Any("/any", func(c *gin.Context) {     c.JSON(http.StatusOK, gin.H{     "msg": "match any route handler",     }) })

WEB篇·第3章[1]·gin框架学习

4.2 路由组

我们可以将拥有共同URL前缀的路由划分为一个路由组。习惯性一对{}包裹同组的路由,这只是为了看着清晰,你用不用{}包裹功能上没什么区别。
路由组也是支持无限嵌套而不损失性能。

accountGrp := r.Group("/account") {     accountGrp.GET("/list", accountList)     accountGrp.POST("/new", accountAdd)     accountGrp.GET("/:id/:name", acntShouldBindingURI)     accountGrp.GET("/:id", accountParam)     accountGrp.GET("/query", accountDefaultQuery)     accountGrp.POST("/json", accountBindJson) }  fileGrp := r.Group("/file") {     fileGrp.GET("/*xxx", fileAbsURL) }

路由原理
Gin框架中的路由使用的是httprouter这个库。其基本原理就是构造一个路由地址的前缀树。

4.3 路由拆分

4.3.1 拆分到多个路由文件,例如:

gin_demo ├── go.mod ├── go.sum ├── main.go └── routers     ├── blog.go     └── shop.go
routers/shop.go中添加一个LoadShop的函数,将shop相关的路由注册到指定的路由器: func LoadShop(e *gin.Engine)  {     e.GET("/hello", helloHandler)     e.GET("/goods", goodsHandler)     e.GET("/checkout", checkoutHandler)     ... }  routers/blog.go中添加一个`LoadBlog的函数,将blog相关的路由注册到指定的路由器: func LoadBlog(e *gin.Engine) {     e.GET("/post", postHandler)     e.GET("/comment", commentHandler)     ... }  在main函数中实现最终的注册逻辑如下: func main() {     r := gin.Default()     routers.LoadBlog(r)     routers.LoadShop(r)     if err := r.Run(); err != nil {         fmt.Println("startup service failed, err:%vn", err)     } }

4.3.2 拆分到不同APP

gin_demo ├── app │   ├── blog │   │   ├── handler.go │   │   └── router.go │   └── shop │       ├── handler.go │       └── router.go ├── go.mod ├── go.sum ├── main.go └── routers     └── routers.go
其中app/blog/router.go用来定义blog相关的路由信息,具体内容如下: func Routers(e *gin.Engine) {     e.GET("/post", postHandler)     e.GET("/comment", commentHandler) }  app/shop/router.go用来定义shop相关路由信息,具体内容如下: func Routers(e *gin.Engine) {     e.GET("/goods", goodsHandler)     e.GET("/checkout", checkoutHandler) }  routers/routers.go中根据需要定义Include函数用来注册子app中定义的路由,Init函数用来进行路由的初始化操作: type Option func(*gin.Engine) var options = []Option{} // 注册app的路由配置 func Include(opts ...Option) {     options = append(options, opts...) } // 初始化 func Init() *gin.Engine {     r := gin.Default()     for _, opt := range options {         opt(r)     }     return r }  main.go中按如下方式先注册子app中的路由,然后再进行路由的初始化: func main() {     // 加载多个APP的路由配置     routers.Include(shop.Routers, blog.Routers)     // 初始化路由     r := routers.Init()     if err := r.Run(); err != nil {         fmt.Println("startup service failed, err:%vn", err)     } }

5. 中间件

类似java中的面向切面(AOP),Gin框架允许开发者在处理请求的过程中,加入用户自己的钩子(Hook)函数。这个钩子函数就叫中间件。
中间件适合处理一些公共的业务逻辑,比如登录认证、权限校验、数据分页、记录日志、耗时统计等。

5.1 定义中间件

// HandlerFunc defines the handler used by gin middleware as return value.
type HandlerFunc func(*Context)
WEB篇·第3章[1]·gin框架学习

5.2 注册中间件

在gin框架中,我们可以为每个路由添加任意数量的中间件。

5.2.1 注册全局中间件

// 方式1 闭包返回匿名gin.HandlerFunc函数即 func(c *gin.Context) // Cost 一个统计耗时请求耗时的中间件 func Cost() gin.HandlerFunc {     return func(c *gin.Context) {         start := time.Now()          // 调用该请求的剩余处理程序         c.Next()          // 不调用该请求的剩余处理程序         // c.Abort()          // 计算耗时         cost := time.Since(start)         fmt.Println("Since", cost)          cost = time.Now().Sub(start) //可更精确的体现[推荐]         fmt.Println("Sub", cost)     } }  r.Use(Cost())  // 方式2 func Cost2(c *gin.Context)  {     start := time.Now()      // 调用该请求的剩余处理程序     c.Next()      // 不调用该请求的剩余处理程序     // c.Abort()      // 计算耗时     cost := time.Since(start)     fmt.Println("Since", cost)      cost = time.Now().Sub(start)     fmt.Println("Sub", cost) }  r.Uset(Cost2)

5.2.2 为某个路由单独注册

(relativePath string, handlers ...HandlerFunc)

// 给路由单独注册中间件(可注册多个) r.Any("/any", Cost2, func(c *gin.Context) {     c.JSON(http.StatusOK, gin.H{     "msg": "match any route handler", })

5.2.3 为路由组注册中间件

Group(relativePath string, handlers ...HandlerFunc)
为路由组注册中间件有以下两种写法。

// 写法1: shopGroup := r.Group("/shop", Cost()) {     shopGroup.GET("/index", func(c *gin.Context) {...})     ... }  // 写法2: shopGroup := r.Group("/shop") shopGroup.Use(Cost()) {     shopGroup.GET("/index", func(c *gin.Context) {...})     ... }

注意 gin中间件中使用goroutine

当在中间件或handler中启动新的goroutine时,不能使用原始的上下文(c *gin.Context),必须使用其只读副本(c.Copy())。

5.3 Abort退出中间件

通过 c.Abort()

func LoginChk(c *gin.Context) {     name, ok := c.Get("name")     if !ok {         c.Abort()     }     ...     c.Next()     ... }
本质 :让c.Next()中的循环立即结束 const abortIndex int8 = math.MaxInt8 / 2  func (c *Context) Abort() {     c.index = abortIndex }  func (c *Context) Next() {     c.index++     for c.index < int8(len(c.handlers)) {         c.handlers[c.index](c)         c.index++     } }

6. 解析请求参数

6.1 GET – Path参数

/:id/:name

fileAbsURL := c.Param("xxx")  id := c.Param("id") name := c.Param("name")

6.2 GET – Query参数

?query=val1&param=val2

id := c.DefaultQuery("id", "0") // 如果取不到值返回给定的默认值; name := c.Query("name") // 如果取不到值返回空串 ""(具体可查看源码解释)

6.3 参数绑定

为了能够更方便的获取请求相关参数,提高开发效率,我们可以基于请求的Content-Type识别请求数据类型并利用反射机制自动提取请求中QueryString、form表单、JSON、XML等参数到结构体中。 下面的示例代码演示了.ShouldBind()强大的功能,它能够基于请求自动提取JSON、form表单和QueryString类型的数据,并把值绑定到指定的结构体对象。

5.3.1 GET – uri参数绑定 – ShouldBindUri

格式 uri:"key" binding:"required"

type Account struct {     No   int    `uri:"id" binding:"required"`     Name string `uri:"name" binding:"required"` } account := new(model.Account) err := c.ShouldBindUri(account)

6.3.2 POST – query/from/json/xml等 – ShouldBind

格式 uri:"urikey" form:"formkey" json:"jsonkey" binding:"required"
// ShouldBind()会根据请求的Content-Type自行选择绑定器
WEB篇·第3章[1]·gin框架学习
WEB篇·第3章[1]·gin框架学习

type Login struct {     User     string `uri:"user" form:"user" json:"user" binding:"required"`     Password string `uri:"pwd" form:"pwd" json:"pass" binding:"required"` }  var login Login // 1) 绑定QueryString示例 (/loginQuery?user=q1mi&password=123456) //router.GET("/loginquery",  // 2) 绑定JSON的示例 ({"user": "q1mi", "password": "123456"}) // router.POST("/loginjson",  // 绑定form表单示例 (user=q1mi password=123456) // router.POST("/loginform"  // ShouldBind()会根据请求的Content-Type自行选择绑定器 if err := c.ShouldBind(&login); err == nil {     fmt.Printf("login info:%#vn", login)     c.JSON(http.StatusOK, gin.H{         "user":     login.User,         "password": login.Password,     }) } else {     c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) }

7. gin服务优雅退出

7.1 优雅退出

优雅退出就是服务端退出命令发出后不是立即退出,而是等待当前还在处理的请求全部处理完毕后再退出程序,是一种对客户端友好的退出方式。而执行Ctrl+C关闭服务端时,会强制结束进程导致正在访问的请求出现问题。
Go 1.8版本之后, http.Server 内置的 Shutdown() 方法就支持优雅地关机,具体示例如下

package main  import (     "context"     "log"     "net/http"     "os"     "os/signal"     "syscall"     "time"      "github.com/gin-gonic/gin" )  func main() {     r := gin.Default()      r.GET("/notify/signal/shutdown", func(c *gin.Context) {         time.Sleep(5 * time.Second)         c.String(http.StatusOK, "Test notify signal to shutdown server !")     })      svr := http.Server{         Addr:    ":8080",         Handler: r,     }      go func() {         // 开启一个goroutine启动服务         if err := svr.ListenAndServe(); err != nil && err != http.ErrServerClosed {             log.Fatalf("listen: %sn", err)         }     }()      // 等待中断信号来优雅地关闭服务器,为关闭服务器操作设置一个5秒的超时     quit := make(chan os.Signal, 1) // 创建一个接收信号的通道      // kill 默认会发送 syscall.SIGTERM 信号     // kill -2 发送 syscall.SIGINT 信号,我们常用的Ctrl+C就是触发系统SIGINT信号     // kill -9 发送 syscall.SIGKILL 信号,但是不能被捕获,所以不需要添加它     // signal.Notify把收到的 syscall.SIGINT或syscall.SIGTERM 信号转发给quit     signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) // 此处不会阻塞     <-quit                                               // 阻塞在此,当接收到上述两种信号时才会往下执行      log.Println("Shutdown Server ...")     // 创建一个10秒超时的context     ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)     defer cancel()      // 5秒内优雅关闭服务(将未处理完的请求处理完再关闭服务),超过5秒就超时退出     if err := svr.Shutdown(ctx); err != nil {         log.Fatal("Server Shutdown: ", err)     }      log.Println("Server exited") }

7.2 优雅重启

优雅关机实现了,那么该如何实现优雅重启呢?
我们可以使用 fvbock/endless 来替换默认的 ListenAndServe启动服务来实现,
示例代码如下:

7.2.1 兼容windows

windows下的信号没有 SIGUSR1、SIGUSR2 等,为了不轻易的抛弃 windows 环境 (不想折腾 mac、ubuntu)。目前用了一个 dirty 的办法解决:
在 go 的安装目录修改 Gosrcsyscalltypes_windows.go,增加如下代码:

var signals = [...]string{     // 这里省略N行。。。。      /** 兼容windows start */     16: "SIGUSR1",     17: "SIGUSR2",     18: "SIGTSTP",     /** 兼容windows end */ }  /** 兼容windows start */ func Kill(...interface{}) {     return; } const (     SIGUSR1 = Signal(16)     SIGUSR2 = Signal(17)     SIGTSTP = Signal(18) ) /** 兼容windows end */
package main  import (     "log"     "net/http"     "time"      "github.com/fvbock/endless"     "github.com/gin-gonic/gin" )  func main() {     router := gin.Default()     router.GET("/", func(c *gin.Context) {         time.Sleep(15 * time.Second)         c.String(http.StatusOK, "hello gin!")     })      // 默认endless服务器会监听下列信号:     // syscall.SIGHUP,syscall.SIGUSR1,syscall.SIGUSR2,syscall.SIGINT,syscall.SIGTERM和syscall.SIGTSTP     // 接收到 SIGHUP 信号将触发`fork/restart` 实现优雅重启(kill -1 pid会发送SIGHUP信号)     // 接收到 syscall.SIGINT或syscall.SIGTERM 信号将触发优雅关机     // 接收到 SIGUSR2 信号将触发HammerTime     // SIGUSR1 和 SIGTSTP 被用来触发一些用户自定义的hook函数      if err := endless.ListenAndServe(":8080", router); err != nil {         log.Fatalf("listen: %sn", err)     }      log.Println("Server exiting") }

如何验证优雅重启的效果呢?
我们通过执行kill -1 pid命令发送syscall.SIGINT来通知程序优雅重启,具体做法如下:
打开终端,go build -o graceful_restart编译并执行./graceful_restart,终端输出当前pid(假设为43682)
将代码中处理请求函数返回的hello gin!修改为hello happy!,再次编译go build -o graceful_restart
打开一个浏览器,访问127.0.0.1:8080/,此时浏览器白屏等待服务端返回响应。
在终端迅速执行kill -1 43682命令给程序发送syscall.SIGHUP信号
等第3步浏览器收到响应信息hello gin!后再次访问127.0.0.1:8080/会收到hello q1mi!的响应。
在不影响当前未处理完请求的同时完成了程序代码的替换,实现了优雅重启。
但是需要注意的是,此时程序的PID变化了,因为endless 是通过fork子进程处理新请求,待原进程处理完当前请求后再退出的方式实现优雅重启的。所以当你的项目是使用类似supervisor的软件管理进程时就不适用这种方式了。

总结
无论是优雅关机还是优雅重启归根结底都是通过监听特定系统信号,然后执行一定的逻辑处理保障当前系统正在处理的请求被正常处理后再关闭当前进程。使用优雅关机还是使用优雅重启以及怎么实现,这就需要根据项目实际情况来决定了。

部分转自互联网,侵权删除联系

赞(0) 打赏
部分文章转自网络,侵权联系删除b2bchain区块链学习技术社区 » WEB篇·第3章[1]·gin框架学习求职学习资料
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址

b2b链

联系我们联系我们