对于 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
 



















