Gin 框架是由 Go 语言编写,基于 net/http 包 封装的一个 Web 框架。
Gin 核心的路由功能是通过 定制版的 HttpRouter 来实现的,具有很高的路由性能。
因为 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")
Gin 框架支持两种路由匹配规则:精确匹配 和 模糊匹配。
例如:路由为 /products/:name
匹配情况如下:
路径 | 匹配情况 |
---|---|
/products/iphone12 | √ |
/products/xiaomi8 | √ |
/products/xiaomi8/music | × |
/products/ | × |
例如:路由为 /products/*name
匹配情况如下:
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)
}
下面代码实现了两个相同的服务,分别监听在不同的端口。
注意,为了不阻塞启动第二个服务,需要把 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)
}
一个 Web 服务应该具有 参数解析、参数校验、逻辑处理、返回结果 4类功能。
HTTP 的请求参数可以存在不同的位置,Gin 是如何解析的呢?
例如:/user/:name
其中, name 就是路径参数。
例如:/welcome?firstname=tian&lastname=dh
其中, firstname 和 lastname 就是查询字符串参数。
例如:curl -X POST -F 'username=tian' -F 'password=123456' http://domain.com/login
其中,username、password 是表单参数。
例如: curl -X POST -H 'Content-Type: application/json' -d '{"username": "admin", "password": "123456"}' http://domain.com/login
其中,Content-Type 就是 HTTP 头参数。
例如: curl -X POST -H 'Content-Type: application/json' -d '{"username": "admin", "password": "123456"}' http://domain.com/login
其中,username、password 是消息体参数。
方式一:直接读取某个参数的值
使用 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:
针对每种参数类型,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 请求的一系列处理插件)能够继续被执行。
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)
}
Gin 支持中间件,HTTP 请求在转发到实际的处理函数之前,会被一系列加载的中间件进行处理。
在中间件中,可以解析 HTTP 请求做一些逻辑处理。例如:跨域处理、或者生成 X-Request-ID 保存在 context 中,以便追踪某个请求。
处理完之后,(1)可以选择中断并返回这次请求。(2)也可以将请求继续转交给下一个中间件处理。
当所有的中间件都处理完之后,请求才会转给路由函数进行处理。
通过中间件,可以实现对所有请求都做统一的处理,提高开发效率,并使我们的代码更简洁。
但是,因为所有请求都需要经过中间件的处理,可能会增加请求延时。
对于中间件特性,有如下建议:
在 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"}))
Logger 中间件会将日志写到 gin.DefaultWriter。
gin.DefaultWriter 默认为 os.Stdout 。
HTTP 请求基本认证(使用用户名、密码进行认证)
Recovery 中间件可以从任何 panic 恢复,并且写入一个 500 状态码。
类似 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")
}
中间件 | 功能 |
---|---|
gin-jwt | JWT 中间件,实现 JWT 认证 |
gin-swagger | 自动生成 Swagger 2.0 格式 的 RESTful API 文档 |
cors | 实现 HTTP 请求跨域 |
sessions | 会话管理中间件 |
authz | 基于 casbin 的授权中间件 |
pprof | gin pprof 中间件 |
go-gin-prometheus | Prometheus metrics exporter |
gzip | 支持 HTTP请求 和 响应 的 gzip 压缩 |
gin-limit | HTTP 请求并发控制中间件 |
requestid | 给每个 Request 生成 uuid,并添加在返回的 X-Request-ID Header 中。 |
这三个高级功能,都可以通过 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,
}))
项目上线后,需要不断迭代来丰富项目功能、修复 Bug 等,这也就意味着,我们要不断的重启服务。
对于 HTTP 服务来说,如果访问量大,重启服务的时候可能还有很多连接没有断开,请求没有完成。如果这时候直接关闭服务,这些连接会直接断掉,请求异常终止,这就会影响到用户体验。
我们期望 HTTP 服务可以在处理完所有请求之后,正常地关闭这些连接,也就是优雅地关闭服务。
有两种方法来优雅关闭 HTTP 服务:
目前使用的比较多的包是 fvbock/endless
可以使用 fvbock/endless 来替换掉 net/http 的 ListenAndServe 方法。
router := gin.Default()
router.GET("/", handler)
...
endless.ListenAndServe(":4242", router)
借助第三方包的好处是可稍微减少一些编码工作量,但缺点是引入了一个新的依赖包。
因此更倾向于自己编码实现。
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")
}