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

 

在Go项目开发中,绝大部分情况下,我们是在写能提供某种功能的后端服务,这些功能以RPC API 接口或者RESTful API接口的形式对外提供,能提供这两种API接口的服务也统称为Web服务。 

 

Web服务的核心功能

将这些功能分成了基础功能和高级功能两大类,并总结在了下面这张图中:

 

要实现一个Web服务,首先我们要选择通信协议和通信格式。在Go项目开发中,有HTTP+JSON 和 gRPC+Protobuf两种组合可选。

因为iam-apiserver主要提供的是REST风格的API接口,所以选择的是HTTP+JSON组合。

Web服务最核心的功能是路由匹配。路由匹配其实就是根据(HTTP方法, 请求路径)匹配到处理这个请求的函数,最终由该函数处理这次请求,并返回结果,过程如下图所示:

一次HTTP请求经过路由匹配,最终将请求交由Delete(c *gin.Context)函数来处理。变量c中存放了这次请求的参数,在Delete函数中,我们可以进行参数解析、参数校验、逻辑处理,最终返回结果。

 

 Web服务还应该能够进行参数解析参数校验逻辑处理返回结果。 

 

在进行HTTP请求时,经常需要针对每一次请求都设置一些通用的操作,比如添加Header、添加RequestID、统计请求次数等,这就要求我们的Web服务能够支持中间件特性。

为了保证系统安全,对于每一个请求,我们都需要进行认证。Web服务中,通常有两种认证方式,一种是基于用户名和密码,一种是基于Token。 

为了方便定位和跟踪某一次请求,需要支持RequestID,定位和跟踪RequestID主要是为了排障。

需要能够处理浏览器的跨域请求。

可以通过学习Gin的官方文档来了解。

 

 

为什么选择Gin框架?

 

在选择Web框架时,我们可以关注如下几点:

  • 路由功能;
  • 是否具备middleware/filter能力;
  • HTTP 参数(path、query、form、header、body)解析和返回;
  • 性能和稳定性;
  • 使用复杂度;
  • 社区活跃度。

按 GitHub Star 数来排名,当前比较火的 Go Web 框架有 Gin、Beego、Echo、Revel 、Martini。经过调研,我从中选择了Gin框架,原因是Gin具有如下特性:

  • 轻量级,代码质量高,性能比较高;
  • 项目目前很活跃,并有很多可用的 Middleware;
  • 作为一个 Web 框架,功能齐全,使用起来简单。

 

Gin是用Go语言编写的Web框架,功能完善,使用简单,性能很高。Gin核心的路由功能是通过一个定制版的HttpRouter来实现的,具有很高的路由性能。

Gin有很多功能,这里我给你列出了它的一些核心功能:

  • 支持HTTP方法:GET、POST、PUT、PATCH、DELETE、OPTIONS。
  • 支持不同位置的HTTP参数:路径参数(path)、查询字符串参数(query)、表单参数(form)、HTTP头参数(header)、消息体参数(body)。
  • 支持HTTP路由和路由分组。
  • 支持middleware和自定义middleware。
  • 支持自定义Log。
  • 支持binding和validation,支持自定义validator。可以bind如下参数:query、path、body、header、form。
  • 支持重定向。
  • 支持basic auth middleware。
  • 支持自定义HTTP配置。
  • 支持优雅关闭。
  • 支持HTTP2。
  • 支持设置和获取cookie。

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

 

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

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

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文件:

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

运行以上代码:

$ go run main.go

打开另外一个终端,请求HTTP接口:

# 创建产品
$ curl -XPOST -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 -XGET 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。

 

HTTP/HTTPS支持

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

insecureServer := &http.Server{
	Addr:         ":8080",
	Handler:      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")

JSON数据格式支持

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

路由匹配

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

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

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

路由分组

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

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

通过将路由分组,可以对相同分组的路由做统一处理。比如上面那个例子,我们可以通过代码

v1 := router.Group("/v1", gin.BasicAuth(gin.Accounts{"foo": "bar", "colin": "colin404"}))

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

一进程多服务

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

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信息。代码如下:

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结构体中。比如,有如下路径参数:

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

我们可以直接读取每个参数:

name := c.Param("name")
action := c.Param("action")

也可以将所有的路径参数,绑定到结构体中:

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。

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

这是另一个非常重要的函数,很多BindXXX函数底层都是调用MustBindWith函数来完成参数绑定的。该函数会根据传入的绑定引擎,将参数绑定到传入的结构体指针中,如果绑定失败,返回错误并终止请求,返回HTTP 400错误。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,这样可以确保我们设置的HTTP Chain(Chain可以理解为一个HTTP请求的一系列处理插件)能够继续被执行。

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

 Web服务可以具备多个高级功能,但比较核心的高级功能是中间件、认证、RequestID、跨域和优雅关停。

中间件

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

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

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

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

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(handle gin.RecoveryFunc):类似Recovery中间件,但是在恢复时还会调用传入的handle方法进行处理。
  • gin.BasicAuth():HTTP请求基本认证(使用用户名和密码进行认证)。

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

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的中间件来实现,例如:

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包

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

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

方法二:编码实现

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

// +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)
	// 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服务。

总结

 

当前比较火的Go Web框架有 Gin、Beego、Echo、Revel、Martini。你可以根据需要进行选择。我比较推荐Gin,Gin也是目前比较受欢迎的Web框架。Gin Web框架支持Web服务的很多基础功能,例如 HTTP/HTTPS、JSON格式的数据、路由分组和匹配、一进程多服务等。

另外,Gin还支持Web服务的一些高级功能,例如 中间件、认证、RequestID、跨域和优雅关停等。

课后练习

  1. 使用 Gin 框架编写一个简单的Web服务,要求该Web服务可以解析参数、校验参数,并进行一些简单的业务逻辑处理,最终返回处理结果。 
  2. 思考下,如何给iam-apiserver的/healthz接口添加一个限流中间件,用来限制请求/healthz的频率。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/516424.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

day63 单调栈part02

503. 下一个更大元素 II 中等 给定一个循环数组 nums &#xff08; nums[nums.length - 1] 的下一个元素是 nums[0] &#xff09;&#xff0c;返回 nums 中每个元素的 下一个更大元素 。 数字 x 的 下一个更大的元素 是按数组遍历顺序&#xff0c;这个数字之后的第一个比它更…

基于隐私保护的可追踪可撤销密文策略属性加密方案论文阅读

论文是2022年发表的A Traceable and Revocable Ciphertext-Policy Attribute-based Encryption Scheme Based on Privacy Protection 摘要 本篇论文提出了一种具有用户撤销、白盒追踪、策略策略隐藏功能的CP-ABE方案。在该方案中密文被分为两个部分&#xff1a;第一个部分是和…

Servlet 的基本理解

Servlet 是JavaEE规范的一种&#xff0c;主要是为了扩展Java作为Web服务的功能&#xff0c;统一接口。由其他内部厂商如tomcat&#xff0c;jetty内部实现web的功能。如一个http请求到来&#xff1a;容器将请求封装为servlet中的HttpServletRequest对象&#xff0c;调用init()&a…

【PduR路由】IPduM模块详细介绍

目录 1.IpduM功能简介 2.IpduM模块依赖的其他模块 2.1RTE (BSW Scheduler) 2.2PDU Router 2.3COM 3.IpduM功能详解 3.1 功能概述 3.2 I-PDU多路复用I-PDU Multiplexing 3.2.1 Definitions and Layout 3.2.2通用功能描述 General 3.2.3模块初始化 Initialization 3.…

安装Docker(CentOS)

Docker 分为 CE 和 EE 两大版本。CE 即社区版&#xff08;免费&#xff0c;支持周期 7 个月&#xff09;&#xff0c;EE 即企业版&#xff0c;强调安全&#xff0c;付费使用&#xff0c;支持周期 24 个月。 Docker CE 分为 stable test 和 nightly 三个更新频道。 官方网站上…

Kubernetes资源ConfigMap

一、ConfigMap的基本概念 1、什么是configMap ConfigMap资源主要为容器注入相关的程序配置信息,用来定制程序的运行方式,比如Redis监听端口、最大客户端连接数。 当定义好一个ConfiqMap资源后,如果Pod需要使用,可以通过通过环境变量、或存储卷的形式将其挂载并加载相关的…

FLink学习(三)-DataStream

一、DataStream 1&#xff0c;支持序列化的类型有 基本类型&#xff0c;即 String、Long、Integer、Boolean、Array复合类型&#xff1a;Tuples、POJOs 和 Scala case classes Tuples Flink 自带有 Tuple0 到 Tuple25 类型 Tuple2<String, Integer> person Tuple2.…

2024年03月CCF-GESP编程能力等级认证Scratch图形化编程四级真题解析

本文收录于专栏《Scratch等级认证CCF-GESP真题解析》,专栏总目录・点这里 一、单选题(共 10 题,每题 2 分,共 30 分) 第1题 小杨的父母最近刚刚给他买了一块华为手表,他说手表上跑的是鸿蒙,这个鸿蒙是?( )。 A、小程序 B、计时器 C、操作系统 D、神话人物 答案:C…

【React】基于JS 3D引擎库实现关系图(图graph)

主角&#xff1a;3D Force-Directed Graph 简介&#xff1a;一个使用ThreeJS/WebGL进行3D渲染的Graph图库 GitHub: https://github.com/vasturiano/3d-force-graph Ps: 较为复杂或节点巨大时&#xff0c;对GPU>CPU消耗较大&#xff0c;同量级节点对比下优于AntV G6和Echarts…

宁波ISO27001认证:信息安全管理的黄金标准

&#x1f603;宁波ISO27001认证&#xff1a;&#x1f916;信息安全管理的&#x1f4a1;黄金标准 随着信息技术&#x1f4bb;的迅猛发展&#xff0c;信息安全&#x1f50f;问题日益凸显&#xff0c;成为企业&#x1f3ec;稳定运营和持续发展的&#x1f4ca;关键因素。在这样&am…

C语言:文件操作(二)

目录 前言 4、文件的顺序读写 4.1fputc 4.2 fgetc 4.3 fputs 4.4 fgets 4.5 fprintf 4.6 fscanf 4.7 fread和fwrite 结&#xff08;二&#xff09; 前言 接者“C语言&#xff1a;文件操作&#xff08;一&#xff09;”往下讲。 本篇文章将介绍C语言的文件操作&#xf…

【算法每日一练]-数论(保姆级教程 篇1 埃氏筛,欧拉筛)

目录 保证给你讲透讲懂 第一种&#xff1a;埃氏筛法 第二种&#xff1a;欧拉筛法 题目&#xff1a;质数率 题目&#xff1a;不喜欢的数 思路&#xff1a; 问题&#xff1a;1~n 中筛选出所有素数&#xff08;质数&#xff09; 有两种经典的时间复杂度较低的筛法&#xff0…

靶向载药脂质体纳米药物载体应用领域

【中文名称】 载药脂质体 【纯 度】 95%以上 【保 存】 4℃保存 【溶 剂】 PBS或者水 【无菌处理】 是 【规 格】 50mg&#xff0c;10mg/ml 【品 牌】 碳水科技&#xff08;Tanshtech&#xff09; 载药脂质体是一种利用脂质双层囊泡包裹药物分子以实现有效…

计算机网络:数据链路层 - 可靠传输协议

计算机网络&#xff1a;数据链路层 - 可靠传输协议 可靠传输概念停止-等待协议 SW回退N帧协议 GBN选择重传协议 SR 可靠传输概念 如下所示&#xff0c;帧在传输过程中受到干扰&#xff0c;产生了误码。接收方的数据链路层&#xff0c;通过真伪中的真检验序列 FCS 字段的值&…

【Linux】-进程知识铺垫①计算机硬件的组织:冯诺依曼体系结构详细解读②关于操作系统对软硬件及用户的意义

目录 ​编辑 1.关于计算机的体系结构 1.1 冯诺依曼体系结构的诞生 2.冯诺依曼体系结构 2.1 cpu:运算器&#xff1a;更多的是让cpu具有特殊的数据计算功能&#xff1a; 2.2 控制器 2.3输入设备 2.4输出设备 3.计算机各个硬件设备之间的关系 4.内存与计算机效率 5.关于为什么总说…

Spoon Taking Problem(c++题解)

题目描述 &#xfffd;N 人が円卓に座っており&#xff0c;各人は反時計回りに順に 1, …, &#xfffd;1, …, N と番号付けられています&#xff0e;各人はそれぞれ左右どちらか一方の利き手を持っています&#xff0e; 円卓上には 1, …, &#xfffd;1, …, N と番号付け…

【Linux】详解动态库链接和加载对可执行程序底层的理解

一、动静态库链接的几种情况 如果我们同时提供动态库和静态库&#xff0c;gcc默认使用的是动态库。如果我们非要使用静态库&#xff0c;要加-static选项。如果我们只提供静态库&#xff0c;那可执行程序没办法&#xff0c;只能对该库进行静态链接&#xff0c;但程序不一定整体…

为移动云数据实现基于可撤销属性组的加密:多代理辅助方法

参考文献为2023年发表的Achieving Revocable Attribute Group-Based Encryption for Mobile Cloud Data: A Multi-Proxy Assisted Approach 动机 对于目前的代理辅助的可撤销基于属性加密来说&#xff0c;外包解密存一些缺点。当多个具有相同属性的用户请求外包转换时&#x…

核心API-Activiti7从入门到专家(3)

背景 今天的说的api&#xff0c;activiti7真有&#xff0c;但真不是这个&#xff1a; 这个是为了云服务&#xff0c;封装的一些api&#xff0c;以后我们还会逐步探讨&#xff0c;今天我们讨论的&#xff0c;是其真正的api&#xff0c;以前是这样的&#xff1a; 是的&#xff0…

ChatGPT 登陆报错:“Oops, an error occurred!” 如何解决?

ChatGPT登录时报错&#xff1a;“Oops, an error occurred!” 说明&#xff1a;哎呀&#xff0c;出错了! 原因&#xff1a; 目前出现这个情况的小伙伴&#xff0c;并非账号被封&#xff0c;多是服务端的问题&#xff0c;比如高峰段&#xff0c;服务端响应不及时&#xff0c;负…