对于 HTTP 服务而言,超时往往是造成服务不可用、甚至系统瘫痪的罪魁祸首。
context 标准库设计思路
为了防止雪崩,context 标准库的解决思路是:在整个树形逻辑链条中,用上下文控制器 Context,实现每个节点的信息传递和共享。
具体操作是:用 Context 定时器为整个链条设置超时时间,时间一到,结束事件被触发,链条中正在处理的服务逻辑会监听到,从而结束整个逻辑链条,让后续操作不再进行。
(也就是一个ctx到处传递)
从图中最后一层的代码 req.ctx = ctx 中看到,每个连接的 Context 最终是放在 request 结构体中的(这也是每个连接的request的ctx赋值的时机)。
所以,每个连接的Context 都是基于 baseContext 复制而来,对应到代码中就是,在为某个连接开启 Goroutine 的时候,为当前连接创建了一个 connContext,这个 connContext 是基于 server 中的 Context 而来,而 server 中 Context 的基础就是 baseContext。
因此,生成最终的 Context 的流程中,net/http 设计了两处可以注入修改的地方,都在 Server 结构里面,一处是 BaseContext,另一处是 ConnContext。
type Server struct {
...
// BaseContext 用来为整个链条创建初始化 Context
// 如果没有设置的话,默认使用 context.Background()
BaseContext func(net.Listener) context.Context
// ConnContext 用来为每个连接封装 Context
// 参数中的 context.Context 是从 BaseContext 继承来的
ConnContext func(ctx context.Context, c net.Conn) context.Context
...
}
最后,我们回看一下 req.ctx 是否能感知连接异常。
是可以的,因为链条中一个父节点为 CancelContext,其 cancelFunc 存储在代表连接的 conn 结构中,连接异常的时候,会触发这个函数句柄。
总结:标准包中ctx基于baseCtx逐级传递到每个连接中,经过多次的withCancel
、withValue,其中baseCtx和ConnCtx都是可以自己注入的。
封装一个自己的Context
在框架中使用Ctx,除了可以控制超时外,常用的有获取请求、返回结果、实现标准库ctx接口,也都要有。
先看一个不封装自定义ctx的控制器代码:
// 控制器, 接收参数为*http.Request, http.ResponseWriter
func Foo1(request *http.Request, response http.ResponseWriter) {
obj := map[string]interface{}{
"data": nil,
}
// 设置控制器 response 的 header 部分
response.Header().Set("Content-Type", "application/json")
// 从请求体中获取参数
foo := request.PostFormValue("foo")
if foo == "" {
foo = "10"
}
fooInt, err := strconv.Atoi(foo)
if err != nil {
response.WriteHeader(500)
return
}
// 构建返回结构
obj["data"] = fooInt
byt, err := json.Marshal(obj)
if err != nil {
response.WriteHeader(500)
return
}
// 构建返回状态,输出返回结构
response.WriteHeader(200)
response.Write(byt)
return
}
可以发现代码较为复杂,如果我们将内部代码封装起来,对外暴露高度语义化的接口函数,则框架的易用性会明显提升。比如:
// 控制器, 接收参数为ctx
func Foo2(ctx *framework.Context) error {
obj := map[string]interface{}{
"data": nil,
}
// 从请求体中获取参数
fooInt := ctx.FormInt("foo", 10)
// 构建返回结构
obj["data"] = fooInt
// 输出返回结构
return ctx.Json(http.StatusOK, obj)
}
这样封装性高,也更优雅易读。
这样就要求总的入口函数处handler(也就是core)的ServceHTTP方法内部将http.request和http.ResponseWriter进行封装。
func (c *Core) ServeHTTP(w http.ResponseWriter, r *http.Request) {
ctx := NewContext(r, w)
handler := c.router["foo"] // r.URL.path
if handler == nil {
return
}
handler(ctx)
}
功能点:
- 获取请求、返回结果
- 实现标准库的Context接口
- 为单个请求设置超时
一个简单的foo处理函数:
func FooControllerHandler(ctx *framework.Context) error {
finish := make(chan struct{}, 1)
panicChan := make(chan interface{}, 1)
durationCtx, cancel := context.WithTimeout(ctx.BaseContext(), 1*time.Second)
defer cancel()
go func() {
defer func() {
if p := recover(); p != nil {
panicChan <- p
}
}()
time.Sleep(10 * time.Second)
ctx.Json(200, "ok")
finish <- struct{}{}
}()
select {
case <-finish:
fmt.Println("finish")
case p := <-panicChan:
ctx.WriterMux().Lock()
defer ctx.WriterMux().Unlock()
log.Println(p)
ctx.Json(500, "panic")
case <-durationCtx.Done():
ctx.WriterMux().Lock()
defer ctx.WriterMux().Unlock()
ctx.Json(500, "time out")
ctx.SetHasTimeout()
}
return nil
}
这里注意有几点:
- Go中每开启一个goroutine,最好使用defer recover()语句来捕获panic异常
- 在写入responseWriter时注意加锁
- 如果超时后,不允许再写入responseWriter