24 | Web服务:Web服务核心功能有哪些,如何实现?

思考并回答以下问题:

Gin是如何支持Web服务基础功能的?

我们创建一个webfeature目录,用来存放示例代码。因为要演示HTTPS的用法,所以需要创建证书文件。具体可以分为两步。

第一步,执行以下命令创建证书:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
cat << 'EOF' > ca.pem
-----BEGIN CERTIFICATE-----
MIICSjCCAbOgAwIBAgIJAJHGGR4dGioHMA0GCSqGSIb3DQEBCwUAMFYxCzAJBgNV
BAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBX
aWRnaXRzIFB0eSBMdGQxDzANBgNVBAMTBnRlc3RjYTAeFw0xNDExMTEyMjMxMjla
Fw0yNDExMDgyMjMxMjlaMFYxCzAJBgNVBAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0
YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQxDzANBgNVBAMT
BnRlc3RjYTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAwEDfBV5MYdlHVHJ7
+L4nxrZy7mBfAVXpOc5vMYztssUI7mL2/iYujiIXM+weZYNTEpLdjyJdu7R5gGUu
g1jSVK/EPHfc74O7AyZU34PNIP4Sh33N+/A5YexrNgJlPY+E3GdVYi4ldWJjgkAd
Qah2PH5ACLrIIC6tRka9hcaBlIECAwEAAaMgMB4wDAYDVR0TBAUwAwEB/zAOBgNV
HQ8BAf8EBAMCAgQwDQYJKoZIhvcNAQELBQADgYEAHzC7jdYlzAVmddi/gdAeKPau
sPBG/C2HCWqHzpCUHcKuvMzDVkY/MP2o6JIW2DBbY64bO/FceExhjcykgaYtCH/m
oIU63+CFOTtR7otyQAWHqXa7q4SbCDlG7DyRFxqG0txPtGvy12lgldA2+RgcigQG
Dfcog5wrJytaQ6UA0wE=
-----END CERTIFICATE-----
EOF

cat << 'EOF' > server.key
-----BEGIN PRIVATE KEY-----
MIICdQIBADANBgkqhkiG9w0BAQEFAASCAl8wggJbAgEAAoGBAOHDFScoLCVJpYDD
M4HYtIdV6Ake/sMNaaKdODjDMsux/4tDydlumN+fm+AjPEK5GHhGn1BgzkWF+slf
3BxhrA/8dNsnunstVA7ZBgA/5qQxMfGAq4wHNVX77fBZOgp9VlSMVfyd9N8YwbBY
AckOeUQadTi2X1S6OgJXgQ0m3MWhAgMBAAECgYAn7qGnM2vbjJNBm0VZCkOkTIWm
V10okw7EPJrdL2mkre9NasghNXbE1y5zDshx5Nt3KsazKOxTT8d0Jwh/3KbaN+YY
tTCbKGW0pXDRBhwUHRcuRzScjli8Rih5UOCiZkhefUTcRb6xIhZJuQy71tjaSy0p
dHZRmYyBYO2YEQ8xoQJBAPrJPhMBkzmEYFtyIEqAxQ/o/A6E+E4w8i+KM7nQCK7q
K4JXzyXVAjLfyBZWHGM2uro/fjqPggGD6QH1qXCkI4MCQQDmdKeb2TrKRh5BY1LR
81aJGKcJ2XbcDu6wMZK4oqWbTX2KiYn9GB0woM6nSr/Y6iy1u145YzYxEV/iMwff
DJULAkB8B2MnyzOg0pNFJqBJuH29bKCcHa8gHJzqXhNO5lAlEbMK95p/P2Wi+4Hd
aiEIAF1BF326QJcvYKmwSmrORp85AkAlSNxRJ50OWrfMZnBgzVjDx3xG6KsFQVk2
ol6VhqL6dFgKUORFUWBvnKSyhjJxurlPEahV6oo6+A+mPhFY8eUvAkAZQyTdupP3
XEFQKctGz+9+gKkemDp7LBBMEMBXrGTLPhpEfcjv/7KPdnFHYmhYeBTBnuVmTVWe
F98XJ7tIFfJq
-----END PRIVATE KEY-----
EOF

cat << 'EOF' > server.pem
-----BEGIN CERTIFICATE-----
MIICnDCCAgWgAwIBAgIBBzANBgkqhkiG9w0BAQsFADBWMQswCQYDVQQGEwJBVTET
MBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50ZXJuZXQgV2lkZ2l0cyBQ
dHkgTHRkMQ8wDQYDVQQDEwZ0ZXN0Y2EwHhcNMTUxMTA0MDIyMDI0WhcNMjUxMTAx
MDIyMDI0WjBlMQswCQYDVQQGEwJVUzERMA8GA1UECBMISWxsaW5vaXMxEDAOBgNV
BAcTB0NoaWNhZ28xFTATBgNVBAoTDEV4YW1wbGUsIENvLjEaMBgGA1UEAxQRKi50
ZXN0Lmdvb2dsZS5jb20wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAOHDFSco
LCVJpYDDM4HYtIdV6Ake/sMNaaKdODjDMsux/4tDydlumN+fm+AjPEK5GHhGn1Bg
zkWF+slf3BxhrA/8dNsnunstVA7ZBgA/5qQxMfGAq4wHNVX77fBZOgp9VlSMVfyd
9N8YwbBYAckOeUQadTi2X1S6OgJXgQ0m3MWhAgMBAAGjazBpMAkGA1UdEwQCMAAw
CwYDVR0PBAQDAgXgME8GA1UdEQRIMEaCECoudGVzdC5nb29nbGUuZnKCGHdhdGVy
em9vaS50ZXN0Lmdvb2dsZS5iZYISKi50ZXN0LnlvdXR1YmUuY29thwTAqAEDMA0G
CSqGSIb3DQEBCwUAA4GBAJFXVifQNub1LUP4JlnX5lXNlo8FxZ2a12AFQs+bzoJ6
hM044EDjqyxUqSbVePK0ni3w1fHQB5rY9yYC5f8G7aqqTY1QOhoUk8ZTSTRpnkTh
y4jjdvTZeLDVBlueZUTDRmy2feY5aZIU18vFDK08dTG0A87pppuv1LNIR3loveU8
-----END CERTIFICATE-----
EOF

第二步,创建main.go文件:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
package main

import (
"fmt"
"log"
"net/http"
"sync"
"time"

"github.com/gin-gonic/gin"
"golang.org/x/sync/errgroup"
)

type Product struct {
Username string `json:"username" binding:"required"`
Name string `json:"name" binding:"required"`
Category string `json:"category" binding:"required"`
Price int `json:"price" binding:"gte=0"`
Description string `json:"description"`
CreatedAt time.Time `json:"createdAt"`
}

type productHandler struct {
sync.RWMutex
products map[string]Product
}

func newProductHandler() *productHandler {
return &productHandler{
products: make(map[string]Product),
}
}

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

// 2. 参数校验
if _, ok := u.products[product.Name]; ok {
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("product %s already exist", product.Name)})
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)
}

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

product, ok := u.products[c.Param("name")]
if !ok {
c.JSON(http.StatusNotFound, gin.H{"error": fmt.Errorf("can not found product %s", c.Param("name"))})
return
}

c.JSON(http.StatusOK, product)
}

func router() http.Handler {
router := gin.Default()
productHandler := newProductHandler()
// 路由分组、中间件、认证
v1 := router.Group("/v1")
{
productv1 := v1.Group("/products")
{
// 路由匹配
productv1.POST("", productHandler.Create)
productv1.GET(":name", productHandler.Get)
}
}

return router
}

func main() {
var eg errgroup.Group

// 一进程多端口
insecureServer := &http.Server{
Addr: ":8080",
Handler: router(),
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
}

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

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

运行以上代码:
1
$ go run main.go

打开另外一个终端,请求HTTP接口:
1
2
3
4
5
6
7
# 创建产品
$ curl -X POST -H "Content-Type: application/json" -d '{"username":"colin","name":"iphone12","category":"phone","price":8000,"description":"cannot afford"}' http://127.0.0.1:8080/v1/products
{"username":"colin","name":"iphone12","category":"phone","price":8000,"description":"cannot afford","createdAt":"2021-06-20T11:17:03.818065988+08:00"}

# 获取产品信息
$ curl -X GET http://127.0.0.1:8080/v1/products/iphone12
{"username":"colin","name":"iphone12","category":"phone","price":8000,"description":"cannot afford","createdAt":"2021-06-20T11:17:03.818065988+08:00"}

示例代码存放地址为webfeature

另外,Gin项目仓库中也包含了很多使用示例,如果你想详细了解,可以参考gin examples

下面,我来详细介绍下Gin是如何支持Web服务基础功能的。

HTTP/HTTPS支持

因为Gin是基于net/http包封装的一个Web框架,所以它天然就支持HTTP/HTTPS。在上述代码中,通过以下方式开启一个HTTP服务:

1
2
3
4
5
6
7
8
insecureServer := &http.Server{
Addr: ":8080",
Handler: router(),
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
}
...
err := insecureServer.ListenAndServe()

通过以下方式开启一个HTTPS服务:
1
2
3
4
5
6
7
8
secureServer := &http.Server{
Addr: ":8443",
Handler: router(),
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
}
...
err := secureServer.ListenAndServeTLS("server.pem", "server.key")

JSON数据格式支持

Gin支持多种数据通信格式,例如application/json、application/xml。可以通过c.ShouldBindJSON函数,将Body中的JSON格式数据解析到指定的Struct中,通过c.JSON函数返回JSON格式的数据。

路由匹配

Gin支持两种路由匹配规则。

第一种匹配规则是精确匹配。例如,路由为/products/:name,匹配情况如下表所示:

第二种匹配规则是模糊匹配。例如,路由为/products/*name,匹配情况如下表所示:

路由分组

Gin通过Group函数实现了路由分组的功能。路由分组是一个非常常用的功能,可以将相同版本的路由分为一组,也可以将相同RESTful资源的路由分为一组。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
v1 := router.Group("/v1", gin.BasicAuth(gin.Accounts{"foo": "bar", "colin": "colin404"}))
{
productv1 := v1.Group("/products")
{
// 路由匹配
productv1.POST("", productHandler.Create)
productv1.GET(":name", productHandler.Get)
}

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

v2 := router.Group("/v2", gin.BasicAuth(gin.Accounts{"foo": "bar", "colin": "colin404"}))
{
productv2 := v2.Group("/products")
{
// 路由匹配
productv2.POST("", productHandler.Create)
productv2.GET(":name", productHandler.Get)
}
}

通过将路由分组,可以对相同分组的路由做统一处理。比如上面那个例子,我们可以通过代码
1
v1 := router.Group("/v1", gin.BasicAuth(gin.Accounts{"foo": "bar", "colin": "colin404"}))

给所有属于v1分组的路由都添加gin.BasicAuth中间件,以实现认证功能。中间件和认证,这里你先不用深究,下面讲高级功能的时候会介绍到。

一进程多服务

我们可以通过以下方式实现一进程多服务:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var eg errgroup.Group
insecureServer := &http.Server{...}
secureServer := &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)
})

上述代码实现了两个相同的服务,分别监听在不同的端口。这里需要注意的是,为了不阻塞启动第二个服务,我们需要把ListenAndServe函数放在goroutine中执行,并且调用eg.Wait()来阻塞程序进程,从而让两个HTTP服务在goroutine中持续监听端口,并提供服务。

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

此外,Web服务还应该具有参数解析、参数校验、逻辑处理、返回结果4类功能,因为这些功能联系紧密,我们放在一起来说。

在productHandler的Create方法中,我们通过c.ShouldBindJSON来解析参数,接下来自己编写校验代码,然后将product信息保存在内存中(也就是业务逻辑处理),最后通过c.JSON返回创建的product信息。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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()})
return
}

// 2. 参数校验
if _, ok := u.products[product.Name]; ok {
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("product %s already exist", product.Name)})
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)
}

那这个时候,你可能会问:HTTP的请求参数可以存在不同的位置,Gin是如何解析的呢?这里,我们先来看下HTTP有哪些参数类型。HTTP具有以下5种参数类型:

  • 路径参数(path)。例如gin.Default().GET("/user/:name",nil),name就是路径参数。
  • 查询字符串参数(query)。例如/welcome?firstname=Lingfei&lastname=Kong,firstname和lastname就是查询字符串参数。
  • 表单参数(form)。例如curl -X POST -F 'username=colin' -F 'password=colin1234' http://mydomain.com/login,username和password就是表单参数。
  • HTTP头参数(header)。例如curl -X POST -H' Content-Type:application/json' -d '{"username":"colin","password":"colin1234"}' http://mydomain.com/login,Content-Type就是HTTP头参数。
  • 消息体参数(body)。例如curl -X POST -H 'Content-Type:application/json' -d '{"username":"colin","password":"colin1234"}' http://mydomain.com/login,username和password就是消息体参数。

Gin提供了一些函数,来分别读取这些HTTP参数,每种类别会提供两种函数,一种函数可以直接读取某个参数的值,另外一种函数会把同类HTTP参数绑定到一个Go结构体中。比如,有如下路径参数:

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

我们可以直接读取每个参数:
1
2
name := c.Param("name")
action := c.Param("action")

也可以将所有的路径参数,绑定到结构体中:
1
2
3
4
5
6
7
8
9
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
}

Gin在绑定参数时,是通过结构体的tag来判断要绑定哪类参数到结构体中的。这里要注意,不同的HTTP参数有不同的结构体tag。

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

针对每种参数类型,Gin都有对应的函数来获取和绑定这些参数。这些函数都是基于如下两个函数进行封装的:

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

非常重要的一个函数,很多ShouldBindXXX函数底层都是调用ShouldBindWith函数来完成参数绑定的。该函数会根据传入的绑定引擎,将参数绑定到传入的结构体指针中,如果绑定失败,只返回错误内容,但不终止HTTP请求。ShouldBindWith支持多种绑定引擎,例如binding.JSON、binding.Query、binding.Uri、binding.Header等,更详细的信息你可以参考binding.go

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

这是另一个非常重要的函数,很多BindXXX函数底层都是调用MustBindWith函数来完成参数绑定的。该函数会根据传入的绑定引擎,将参数绑定到传入的结构体指针中,如果绑定失败,返回错误并终止请求,返回HTTP400错误。MustBindWith所支持的绑定引擎跟ShouldBindWith函数一样。

Gin基于ShouldBindWith和MustBindWith这两个函数,又衍生出很多新的Bind函数。这些函数可以满足不同场景下获取HTTP参数的需求。Gin提供的函数可以获取5个类别的HTTP参数。

  • 路径参数:ShouldBindUri、BindUri;
  • 查询字符串参数:ShouldBindQuery、BindQuery;
  • 表单参数:ShouldBind;
  • HTTP头参数:ShouldBindHeader、BindHeader;
  • 消息体参数:ShouldBindJSON、BindJSON等。

每个类别的Bind函数,详细信息你可以参考Gin提供的Bind函数

这里要注意,Gin并没有提供类似ShouldBindForm、BindForm这类函数来绑定表单参数,但我们可以通过ShouldBind来绑定表单参数。当HTTP方法为GET时,ShouldBind只绑定Query类型的参数;当HTTP方法为POST时,会先检查content-type是否是json或者xml,如果不是,则绑定Form类型的参数。

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

在Go项目开发中,我建议使用ShouldBindXXX,这样可以确保我们设置的HTTPChain(Chain可以理解为一个HTTP请求的一系列处理插件)能够继续被执行。

Gin是如何支持Web服务高级功能的?

上面介绍了Web服务的基础功能,这里我再来介绍下高级功能。Web服务可以具备多个高级功能,但比较核心的高级功能是中间件、认证、RequestID、跨域和优雅关停。

中间件

Gin支持中间件,HTTP请求在转发到实际的处理函数之前,会被一系列加载的中间件进行处理。在中间件中,可以解析HTTP请求做一些逻辑处理,例如:跨域处理或者生成X-Request-ID并保存在context中,以便追踪某个请求。处理完之后,可以选择中断并返回这次请求,也可以选择将请求继续转交给下一个中间件处理。当所有的中间件都处理完之后,请求才会转给路由函数进行处理。具体流程如下图:

通过中间件,可以实现对所有请求都做统一的处理,提高开发效率,并使我们的代码更简洁。但是,因为所有的请求都需要经过中间件的处理,可能会增加请求延时。对于中间件特性,我有如下建议:

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

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

1
2
3
4
router := gin.New()
router.Use(gin.Logger(), gin.Recovery()) // 中间件作用于所有的HTTP请求
v1 := router.Group("/v1").Use(gin.BasicAuth(gin.Accounts{"foo": "bar", "colin": "colin404"})) // 中间件作用于v1 group
v1.POST("/login", Login).Use(gin.BasicAuth(gin.Accounts{"foo": "bar", "colin": "colin404"})) //中间件只作用于/v1/login API接口

Gin框架本身支持了一些中间件。

  • gin.Logger():Logger中间件会将日志写到gin.DefaultWriter,gin.DefaultWriter默认为os.Stdout。
  • gin.Recovery():Recovery中间件可以从任何panic恢复,并且写入一个500状态码。
  • gin.CustomRecovery(handlegin.RecoveryFunc):类似Recovery中间件,但是在恢复时还会调用传入的handle方法进行处理。
  • gin.BasicAuth():HTTP请求基本认证(使用用户名和密码进行认证)。

另外,Gin还支持自定义中间件。中间件其实是一个函数,函数类型为gin.HandlerFunc,HandlerFunc底层类型为func(*Context)。如下是一个Logger中间件的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
package main

import (
"log"
"time"

"github.com/gin-gonic/gin"
)

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()
r.Use(Logger())

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

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

// Listen and serve on 0.0.0.0:8080
r.Run(":8080")
}

另外,还有很多开源的中间件可供我们选择,我把一些常用的总结在了表格里:

认证、RequestID、跨域

认证、RequestID、跨域这三个高级功能,都可以通过Gin的中间件来实现,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
router := gin.New()

// 认证
router.Use(gin.BasicAuth(gin.Accounts{"foo": "bar", "colin": "colin404"}))

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

// 跨域
// CORS for https://foo.com and https://github.com origins, allowing:
// - PUT and PATCH methods
// - Origin header
// - Credentials share
// - Preflight 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,
}))

优雅关停

Go项目上线后,我们还需要不断迭代来丰富项目功能、修复Bug等,这也就意味着,我们要不断地重启Go服务。对于HTTP服务来说,如果访问量大,重启服务的时候可能还有很多连接没有断开,请求没有完成。如果这时候直接关闭服务,这些连接会直接断掉,请求异常终止,这就会对用户体验和产品口碑造成很大影响。因此,这种关闭方式不是一种优雅的关闭方式。

这时候,我们期望HTTP服务可以在处理完所有请求后,正常地关闭这些连接,也就是优雅地关闭服务。我们有两种方法来优雅关闭HTTP服务,分别是借助第三方的Go包和自己编码实现。

方法一:借助第三方的Go包

如果使用第三方的Go包来实现优雅关闭,目前用得比较多的包是fvbock/endless。我们可以使用fvbock/endless来替换掉net/http的ListenAndServe方法,例如:

1
2
3
4
router := gin.Default()
router.GET("/", handler)
// [...]
endless.ListenAndServe(":4242", router)

方法二:编码实现

借助第三方包的好处是可以稍微减少一些编码工作量,但缺点是引入了一个新的依赖包,因此我更倾向于自己编码实现。Go1.8版本或者更新的版本,http.Server内置的Shutdown方法,已经实现了优雅关闭。下面是一个示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
// +build go1.8

package main

import (
"context"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"

"github.com/gin-gonic/gin"
)

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

// Initializing the server in a goroutine so that
// it won't block the graceful shutdown handling below
go func() {
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("listen: %s\n", err)
}
}()

// Wait for interrupt signal to gracefully shutdown the server with
// a timeout of 5 seconds.
quit := make(chan os.Signal, 1)
// kill (no param) default send syscall.SIGTERM
// kill -2 is syscall.SIGINT
// kill -9 is syscall.SIGKILL but can't be catch, so don't need add it
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Println("Shutting down server...")

// The context is used to inform the server it has 5 seconds to finish
// the request it is currently handling
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")
}

上面的示例中,需要把srv.ListenAndServe放在goroutine中执行,这样才不会阻塞到srv.Shutdown函数。因为我们把srv.ListenAndServe放在了goroutine中,所以需要一种可以让整个进程常驻的机制。

这里,我们借助了无缓冲channel,并且调用signal.Notify函数将该channel绑定到SIGINT、SIGTERM信号上。这样,收到SIGINT、SIGTERM信号后,quilt通道会被写入值,从而结束阻塞状态,程序继续运行,执行srv.Shutdown(ctx),优雅关停HTTP服务。

课后练习

如何给iam-apiserver的/healthz接口添加一个限流中间件,用来限制请求/healthz的频率。

0%