Go Web 服务的核心功能


Web 服务的核心功能


简介

Web 服务有很多功能,这些主要功能分为基础功能(如 HTTP/HTTPS、JSON 格式的数据、路由分组和匹配、一进程多服务 等)和高级功能(如 中间件、认证、RequestID、跨域和优雅关停 等)两大类,具体的功能如下图所示:

Web 服务功能图


基础功能


通信协议与格式

实现一个 Web 服务,首先要选择 通信协议通信格式 ,在 Go 项目开发中,有 HTTP+JSONgRPC+Protobuf 两种组合可选,REST 风格的 API 接口选择的是 HTTP+JSON 组合;RPC API 接口选择的是gRPC+Protobuf


路由匹配功能

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

路由匹配功能图

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

一进程多服务功能

有时需在一个服务进程中同时开启 HTTP 服务的 80 端口和 HTTPS443 端口。这样就可以对内的服务,访问 80 端口,简化服务访问复杂度;对外的服务,访问更为安全的 HTTPS 服务。显然,没必要为相同功能启动多个服务进程,所以就需要 Web 服务能够支持一进程多服务的功能。


业务处理功能

开发 Web 服务最核心的诉求是输入一些参数,校验通过后,进行业务逻辑处理,然后返回结果,所以 Web 服务还应该能够进行参数解析、参数校验、逻辑处理、返回结果。


高级功能


支持中间件功能

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


安全认证功能

为了保证系统安全,对于每一个请求都应进行认证,在 Web 服务中,通常有两种认证方式,一种是基于 用户名和密码 ,另一种是基于 Token ,认证通过之后,才可以进行对请求的处理。


支持 RequestID 功能

为了方便定位和跟踪某一次请求,所以 Web 服务需要支持 RequestID ,定位和跟踪 RequestID 主要是方便排除障碍。


处理浏览器的跨域请求功能

在前后端分离的架构中,前端访问地址和后端访问地址一般是不同的,浏览器为了安全,会针对这种情况设置跨域请求,所以 Web 服务需要能够处理浏览器的跨域请求。

以上介绍了 Web 服务的核心功能,还有更多其它功能,可以参考 Gin 官网


Gin 框架的核心功能


简介

Web 服务有很多核心功能可以基于 Go 语言中的 net/http 包进行封装,但在实际的项目开发中, 更多会选择使用基于 net/http 包进行封装的优秀开源 Web 框架。

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

  • 路由功能;

  • 是否具备 middleware/filter 能力;

  • HTTP 参数(path、query、form、header、body)解析和返回;

  • 性能和稳定性;

  • 使用复杂度;

  • 社区活跃度;

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

Gin 框架具有如下特性:

  • 轻量级,代码质量高,性能比较高;

  • 项目开源很活跃,并有很多可用的中间件;

  • Web 框架的功能齐全,使用简单易上手;

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 ;


基础功能


支持 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")

支持多种数据通信格式功能

Gin 支持多种数据通信格式(如 application/jsonapplication/xml 格式),通过调用 ShouldBindJSON() 方法,将 Body 中的 JSON 格式数据解析到指定的 Struct 中,再通过调用 JSON() 方法返回 JSON 格式的数据。


路由匹配功能

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

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

路径匹配情况
/products/iphone12匹配
/products/xiaomi13匹配
/products/xiaomi13/game不匹配
/products/不匹配

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

路径匹配情况
/products/iphone12匹配
/products/xiaomi13匹配
/products/xiaomi13/game匹配
/products/匹配

路由分组功能

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 分组的路由都添加 BasicAuth 中间件以实现认证功能,关键的程序代码如下:

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

一进程多服务功能

可以通过以下方式实现一进程多服务,关键的程序代码如下:

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 中执行,并且调用 Wait() 方法来阻塞程序进程,从而让两个 HTTPS 服务在 goroutine 中持续监听端口,并提供服务)。


业务处理功能

productHandlerCreate() 方法中,通过调用 ShouldBindJSON() 方法来解析参数,接下来自己编写校验代码,然后将 product 信息保存在内存中(也就是业务逻辑处理),最后通过 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 的请求参数可以存在不同的位置,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)

可以直接调用 Param() 方法读取每个参数,关键的程序代码如下:

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

还可以调用 ShouldBindUri() 方法将所有的路径参数,绑定到结构体中,关键的程序代码如下:

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 ,具体参考下表:

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

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

  • ShoulBindWith() 方法
func (c *Context) ShouldBindWith(obj any, b binding.Binding) error

大多数 ShouldBindXXX() 方法底层都是调用 ShouldBindWith() 方法来完成参数绑定的,该方法会根据传入的绑定引擎,将参数绑定到传入的结构体指针中,如果绑定失败,只返回错误内容,但不终止 HTTP 请求。ShouldBindWith() 方法支持多种绑定引擎(如 binding.JSONbinding.Querybinding.Uribinding.Header)。

  • MustBindWith() 方法
func (c *Context) MustBindWith(obj any, 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 等

(1)路径参数

  • 获取指定的路径参数

Param(key string) string :如果指定的参数存在则返回其值,不存在则返回空字符串 " "

  • 绑定路径参数

ShouldBindUri(obj interface{}) error :绑定路径参数到传入的结构体指针中,绑定出错时,返回错误内容。

BindUri(obj interface{}) error :绑定路径参数到传入的结构体指针中,绑定出错时,会终止请求,并返回 HTTP 400 错误。

(2)查询字符串参数

  • 获取指定的查询字符串参数

Query(key string) string :如果指定的参数 key 存在则返回其值,不存在则返回空字符串 " "

DefaultQuery(key, defaultValue string) string :如果指定的 key 存在则返回其值,不存在则返回指定的默认字符串 defaultValue。

GetQuery(key string) (string, bool) :同 Query() ,但是当 key 存在时,会额外返回一个 true 值,当 key 不存在时,会额外返回一个 false 值。

QueryArray(key string) []string :返回指定 key 的 slice ,例如: GET /?name=name1&name=name2 ,则返回:[“name1”,:“name2”],如果key 不存在,则返回 []string{}。

GetQueryArray(key string) ([]string, bool):同 QueryArray() ,但是当 key 存在时,会额外返回一个 true 值,当 key 不存在时,会额外返回一个 false 值。

QueryMap(key string) map[string]string :返回 key 的 map 值,例如:POST /post?ids[a]=1234&ids[b]=hello ,使用 QueryMap 返回的 map 值为:ids: map[b:hello a:1234]。

GetQueryMap(key string) (map[string]string, bool) :同 QueryMap ,但是当 key 存在时,会额外返回一个 true 值,当 key 不存在时,会额外返回一个 false 值。

  • 绑定查询字符串参数

ShouldBindQuery(obj interface{}) error :绑定查询字符串参数到传入的结构体指针中,绑定出错时,返回错误内容。

BindQuery(obj interface{}) error :绑定查询字符串参数到传入的结构体指针中,绑定出错时,会终止请求,并返回 HTTP 400 错误。

(3)表单参数

  • 获取指定的表单参数

PostForm(key string) string :如果指定的参数 key 存在则返回其值,不存在则返回空字符串 " "

DefaultPostForm(key, defaultValue string) :如果指定的 key 存在则返回其值,不存在则返回指定的默认字符串 defaultValue。

GetPostForm(key string) (string, bool) :同 PostForm() ,但是当 key 存在时,会额外返回一个 true 值,当 key 不存在时,会额外返回一个 false 值。

PostFormArray(key string) []string :返回指定 key 的 slice ,如果 key 不存在,则返回 []string{}。

GetPostFormArray(key string) ([]string, bool) :同 PostFormArray() ,但是当 key 存在时,会额外返回一个 true 值,当 key 不存在时,会额外返回一个 false 值。

PostFormMap(key string) map[string]string :返回 key 的 map 值。

GetPostFormMap(key string) (map[string]string, bool) :同 PostFormMap ,但是当 key 存在时,会额外返回一个 true 值,当 key 不存在时,会额外返回一个 false 值。

(4) HTTP 头参数

  • 获取指定的 HTTP 头参数

GetHeader(key string) string :如果指定的参数存在则返回其值,不存在则返回空字符串 " "

  • 绑定 HTTP 头参数

ShouldBindHeader(obj interface{}) error :绑定 HTTP 头参数到传入的结构体指针中,绑定出错时,返回错误内容。

BindHeader(obj interface{}) error :绑定 HTTP 头参数到传入的结构体指针中,绑定出错时,会终止请求,并返回 HTTP 400 错误。

(5)消息体参数

ShouldBind(obj interface{}) error :检查 Content-Type 并选择适配的绑定引擎,不同的 Content-Type ,选择的绑定引擎不同,Content-Type 当前支持 application/json、application/xml 。

Bind(obj interface{}) error:同 ShouldBind(),但是当绑定失败时,会终止 HTTP 请求,并设置 Content-Type=“text/plain” ,返回 HTTP 400 错误。

BindJSON(obj interface{}) error :MustBindWith(obj, binding.JSON) 的简单封装。

ShouldBindJSON(obj interface{}) error :ShouldBindWith(obj, binding.JSON) 的简单封装。

BindXML(obj interface{}) error :MustBindWith(obj, binding.XML) 的简单封装。

ShouldBindXML(obj interface{}) error :ShouldBindWith(obj, binding.XML) 的简单封装。

BindYAML(obj interface{}) error :MustBindWith(obj, binding.YAML) 的简单封装。

ShouldBindYAML(obj interface{}) error:ShouldBindWith(obj, binding.YAML) 的简单封装。

ShouldBindBodyWith(obj interface{}, bb binding.BindingBody) (err error):类似于 ShouldBindWith() ,但是会把 body 存储在 context 中,方便以后再次使用,该函数在绑定前,会读取 body ,如果后面不需要再使用 body ,则可以直接调用 ShouldBindWith() 完成绑定功能,这样可以提高性能。


ShouldBindXXX() 在绑定出错时,返回错误内容;BindXXX() 在绑定出错时,会终止请求,并返回 HTTP 400 错误。

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

因此,调用ShouldBind() 方法可以绑定 Form 类型的参数,但前提是 HTTP 方法是 POST 并且 content-type 不是 application/jsonapplication/xml 类型。

在Go项目开发中,调用 ShouldBindXXX() 方法可以确保设置的 HTTP ChainChain 可以理解为一个 HTTP 请求的一系列处理插件)能够继续被执行。


实现基础功能的示例

首先创建一个名为 web 的目录用来存放示例代码,然后创建证书文件,编写一个 HTTPS Server 程序,具体的步骤如下:

  • 第一步:创建证书文件,依次执行以下命令:
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
  • 第二步:编写程序实现功能,该程序的具体代码如下:
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)
		}
}

运行该程序后,打开另一个终端请求 HTTPS 接口验证 Web 服务的基础功能。

(1)创建产品。

  • 创建一个产品,执行以下命令:
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":"2023-02-03T11:17:03.818065988+08:00"}

(2)获取产品信息。

  • 获取创建的产品信息,输入以下命令:
curl -XGET http://127.0.0.1:8080/v1/products/iphone12
  • 输出的结果具体内容如下:
{"username":"colin","name":"iphone12","category":"phone","price":8000,"description":"cannot afford","createdAt":"2023-02-03T11:17:03.818065988+08:00"}

另外,在Gin 项目仓库中包含了很多使用示例,详情参考 gin examples


高级功能


支持中间件功能

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

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

中间件处理流程图

通过中间件可以实现对所有请求都做统一的处理,提高开发效率,并使程序代码更简洁,但因为所有的请求都需要经过中间件的处理,可能会增加请求延时,所以,编写中间件程序应该注意如下几点:

  • 中间件做成可加载的,通过配置文件指定程序启动时加载哪些中间件。

  • 只将一些通用的、必要的功能做成中间件。

  • 编写的中间件要保证中间件的代码质量和性能。

Gin 中,可以通过 gin.EngineUse() 方法来加载中间件,中间件可以加载到不同的位置上,而且不同的位置作用范围也不同,例如以下关键的程序代码:

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():类似 Recovery 中间件,但是在恢复时还会调用传入的 handle 方法进行处理。

  • gin.BasicAuth():HTTP 请求基本认证(使用用户名和密码进行认证)。

Gin 支持自定义中间件,中间件其实是一个函数,函数类型为gin.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")
}

另外,还有很多开源的中间件可供选择,一些常用的中间件参考如下表:

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

认证、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,
}))

优雅关停

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

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

如果使用第三方的 Go包来实现优雅关闭,目前用得比较多的包是 fvbock/endless ,通过调用 fvbock/endless 包中的 ListenAndServe() 方法来替换掉 net/http 包中的 ListenAndServe() 方法,例如如下关键的程序代码:

router := gin.Default()
router.GET("/", handler)
// [...]
endless.ListenAndServe(":4242", router)
  • 方法二:编码实现

借助第三方包的好处是可以稍微减少一些编码工作量,但缺点是引入了一个新的依赖包,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() {
		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 并且调用 Notify() 函数将该 channel 绑定到 SIGINTSIGTERM 信号上,在收到 SIGINTSIGTERM 信号后,quilt 通道会被写入值,从而结束阻塞状态,程序继续运行,执行 srv.Shutdown(ctx) 部分,优雅关停 HTTPS 服务。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

물の韜

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值