这是我参与「第五届⻘训营 」笔记创作活动的第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.Request和http.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中,分别有JSON、XML、Form、FormPost、FormMultipart、Uri、ProtoBuf、MsgPack、YAML、Header几种 类型实现了Binding接口,可用于在请求中构造结构体(tag标签必须与上面的类型所匹配)来绑定请求数据。Gin具体提供两类绑定方法:MustBind和ShouldBind,前者如果绑定发生了错误,则请求终止,并响应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这里的代码