Web框架中的Context | ⻘训营笔记

255 阅读5分钟

这是我参与「第五届⻘训营 」笔记创作活动的第5天。今天学习了Web开发中常用的框架,即常说的三大件,Web框架、orm框架、rpc框架,分别对应字节常用的Hertz、gorm、kitex。不过除了使用框架,研究框架也是一件很有趣的事情,这能让我们看清业务逻辑背后的代码实现,希望借此能有一些能力上的提升吧。

请求的上下文(Context)是对于golang中网络请求以及回复的高一层抽象,也是一个网络框架里最重要的一个部分,各个框架有自己不同的实现,不过意思上大同小异,通过了解Context,可以快速了解一个框架的用法,也能明白网络框架存在的意义。

Go 原生http库

这个是Golang的Web版'hello world',可以看到处理请求的函数func greet(w http.ResponseWriter, r *http.Request),这是原生处理请求返回回复的方法,

package main

import (
	"fmt"
	"net/http"
	"time"
)

func greet(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "Hello World! %s", time.Now())
}

func main() {
	http.HandleFunc("/", greet)
	http.ListenAndServe(":8080", nil)
}

对Web服务来说,无非是根据请求*http.Request,构造响应http.ResponseWriter。但是这两个对象提供的接口粒度太细,比如我们要构造一个完整的响应,需要考虑消息头(Header)和消息体(Body),而 Header 包含了状态码(StatusCode),消息类型(ContentType)等几乎每次请求都需要设置的信息。因此,如果不进行有效的封装,那么框架的用户将需要写大量重复,繁杂的代码,而且容易出错。针对常用场景,能够高效地构造出 HTTP 响应是一个好的框架必须考虑的点。

用返回 JSON 数据作比较,感受下封装前后的差距。

封装前

obj = map[string]interface{}{
   "name": "geektutu",
   "password": "1234",
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
encoder := json.NewEncoder(w)
if err := encoder.Encode(obj); err != nil {
   http.Error(w, err.Error(), 500)
}

VS 封装后:

c.JSON(http.StatusOK, gee.H{
    "username": c.PostForm("username"),
    "password": c.PostForm("password"),
})

针对使用场景,封装*http.Requesthttp.ResponseWriter的方法,简化相关接口的调用,只是设计 Context 的原因之一。对于框架来说,还需要支撑额外的功能。例如,将来解析动态路由/hello/:name,参数:name的值放在哪呢?再比如,框架需要支持中间件,那中间件产生的信息放在哪呢?Context 随着每一个请求的出现而产生,请求的结束而销毁,和当前请求强相关的信息都应由 Context 承载。因此,设计 Context 结构,扩展性和复杂性留在了内部,而对外简化了接口。路由的处理函数,以及将要实现的中间件,参数都统一使用 Context 实例, Context 就像一次会话的百宝箱,可以找到任何东西。

一般的Context实现思路

以下是gin的context设计

// Context is the most important part of gin. It allows us to pass variables between middleware,
// manage the flow, validate the JSON of a request and render a JSON response for example.
type Context struct {
	writermem responseWriter
	Request   *http.Request
	Writer    ResponseWriter

	Params   Params
	handlers HandlersChain
	index    int8
	fullPath string

	engine       *Engine
	params       *Params
	skippedNodes *[]skippedNode

	// This mutex protect Keys map
	mu sync.RWMutex

	// Keys is a key/value pair exclusively for the context of each request.
	Keys map[string]interface{}

	// Errors is a list of errors attached to all the handlers/middlewares who used this context.
	Errors errorMsgs

	// Accepted defines a list of manually accepted formats for content negotiation.
	Accepted []string

	// queryCache use url.ParseQuery cached the param query result from c.Request.URL.Query()
	queryCache url.Values

	// formCache use url.ParseQuery cached PostForm contains the parsed form data from POST, PATCH,
	// or PUT body parameters.
	formCache url.Values

	// SameSite allows a server to define a cookie attribute making it impossible for
	// the browser to send this cookie along with cross-site requests.
	sameSite http.SameSite
}

首先来说,gin利用Pool来初始化context,这可以提升框架性能。Pool 模式是一种创建和提供可供使用的固定数量实例或Pool 实例的方法。 它通常用于约束创建昂贵的场景 (如数据库连接),以便只创建固定数量的实例,但不确定数量的操作仍然可以请求访问这些场景。 对于 Go 语言的 sync.Pool ,这种数据类型可以被多个 goroutine 安全地使用 。

元数据管理

这个模块比较简单, 就是从gin.Context中Set Key-Value, 以及各种个样的Get方法, 如GetBool, GetString等

gin通过一个map实现了这些功能

// Keys is a key/value pair exclusively for the context of each request.
Keys map[string]interface{}

获取请求参数

  • param数据

    • 当路由是这种形式时router.GET("/user/:id", func(c *gin.Context) {}),可以通过 id := c.Param("id")来获取到id的参数值
    // 返回URL参数
    func (c *Context) Param(key string) string {
            return c.Params.ByName(key)
    }
    
  • Query

    • /welcome?firstname=Jane&lastname=Doe这样一个路由, firstname, lastname即是Querystring parameters, 要获取他们就需要使用Query相关函数.
    c.Query("firstname") // Jane
    c.Query("lastname") // Doe
    

    这些方法的实现是利用net/http的Request的方法实现的

  • PostForm

    • 对于POST, PUT等这些能够传递参数Body的请求, 要获取其参数, 需要使用PostForm
    POST /user/1
    
    {
        "name":manu,
        "message":this_is_great
    }
    
    name := c.PostForm("name")
    message := c.PostForm("message")
    

    这些相关的方法是实现还是利用net/http的Request的方法实现的

  • Bind

    • Gin中,分别有JSONXMLFormFormPostFormMultipartUriProtoBufMsgPackYAMLHeader几种 类型实现了Binding接口,可用于在请求中构造结构体(tag标签必须与上面的类型所匹配)来绑定请求数据。 Gin具体提供两类绑定方法:MustBindShouldBind,前者如果绑定发生了错误,则请求终止,并响应400状态码;后者如果发生了绑定错误,Gin会返回错误并由开发者处理错误和请求。
  • render

    • 做api常用到的其实就是gin封装的各种render. 目前支持的有:
   func (c *Context) JSON(code int, obj interface{})
   func (c *Context) Protobuf(code int, obj interface{})
   func (c *Context) YAML(code int, obj interface{}) ...

当然我们可以自定义渲染, 只要实现func (c *Context) Render(code int, r render.Render)即可.

这里我们常用的是一个方法是: gin.H{"error": 111}. 这个结构相当实用, 各种render都支持. 其实这个结构很简单就是type H map[string]interface{}, 当我们要从map转换各种各样结构时, 不妨参考gin这里的代码