实战:用取消参数使 Go net/http 服务更灵活( 三 )
$ Go run server.go2019/12/29 17:20:03 Slow API call done after 4 seconds.
这个现象表明:虽然 1 秒之后请求超时了 , 但是服务仍然完整地处理了请求 。 这就是在 4 秒之后才打出日志的原因 。
虽然在这个例子里问题很简单 , 但是类似的现象在生产中可能变成一个严重的问题 。 例如 , 当 slowAPICall 函数开启了几个百个协程 , 每个协程都处理一些数据时 。 或者当它向不同系统发出多个不同的 API 发出请求时 。 这种耗时长的的进程 , 它们的请求方/客户端并不会使用服务端的返回结果 , 会耗尽你系统的资源 。
所以 , 我们怎么保护系统 , 使之不会出现类似的未优化的超时或取消请求呢?
上下文超时和取消Go 有一个包名为 `context`[8] 专门处理类似的场景 。
context 包在 Go 1.7 版本中提升为标准库 , 在之前的版本中 , 以`golang.org/x/net/context`[9] 的路径作为 Go Sub-repository Packages[10]出现 。
这个包定义了 Context 类型 。 它最初的目的是保存不同 API 和不同处理的截止时间、取消信号和其他请求相关的值 。 如果你想了解关于 context 包的其他信息 , 可以阅读 Golang's blog[11] 中的 “Go 并发模式:Context”(译注:Go Concurrency Patterns: Context) .
net/http 包中的的 Request 类型已经有 context 与之绑定 。 从 Go 1.7 开始 , Request 新增了一个返回请求的上下文的 `Context` 方法[12] 。 对于进来的请求 , 在客户端关闭连接、请求被取消(HTTP/2 中)或 ServeHTTP 方法返回后 , 服务端会取消上下文 。
我们期望的现象是 , 当客户端取消请求(输入了 CTRL + C)或一段时间后TimeoutHandler 继续执行然后终止请求时 , 服务端会停止后续的处理 。 进而关闭所有的连接 , 释放所有被运行中的处理进程(及它的所有子协程)占用的资源 。
我们把 Context 作为参数传给 slowAPICall 函数:
func slowAPICall(ctx context.Context) string { d := rand.Intn(5) select { case <-time.After(time.Duration(d) * time.Second):log.Printf("Slow API call done after %d seconds.\n", d)return "foobar" }}func slowHandler(w http.ResponseWriter, r *http.Request) { result := slowAPICall(r.Context()) io.WriteString(w, result+"\n")}
在例子中我们利用了请求上下文 , 实际中怎么用呢?`Context` 类型[13]有个 Done 属性 , 类型为 <-chan struct{} 。 当进程处理完成时 , Done 关闭 , 此时表示上下文应该被取消 , 而这正是例子中我们需要的 。
我们在 slowAPICall 函数中用 select 处理 ctx.Done 通道 。 当我们通过 Done 通道接收一个空的 struct 时 , 意味着上下文取消 , 我们需要让 slowAPICall 函数返回一个空字符串 。
func slowAPICall(ctx context.Context) string { d := rand.Intn(5) select { case <-ctx.Done():log.Printf("slowAPICall was supposed to take %s seconds, but was canceled.", d)return ""//time.After() 可能会导致内存泄漏 case <-time.After(time.Duration(d) * time.Second):log.Printf("Slow API call done after %d seconds.\n", d)return "foobar" }}
(这就是使用 select 而不是 time.Sleep -- 这里我们只能用 select 处理 Done 通道 。 )
在这个简单的例子中 , 我们成功得到了结果 -- 当我们从 Done 通道接收值时 , 我们打印了一行日志到 STDOUT 并返回了一个空字符串 。 在更复杂的情况下 , 如发送真实的 API 请求 , 你可能需要关闭连接或清理文件描述符 。
我们再启动服务 , 发送一个 cRUL 请求:
# The cURL command:$ curl localhost:8888Timeout!# The server output:$ Go run server.go2019/12/30 00:07:15 slowAPICall was supposed to take 2 seconds, but was canceled.
检查输出:我们发送了 cRUL 请求到服务 , 它耗时超过 1 秒 , 服务取消了 slowAPICall 函数 。 我们几乎不需要写任何代码 。 TimeoutHandler 为我们代劳了 -- 当处理耗时超过预期时 , TimeoutHandler 终止了处理进程并取消请求上下文 。
TimeoutHandler 是在 `timeoutHandler.ServeHTTP` 方法[14] 中取消上下文的:
// Taken from: #L3217-L3263func (h *timeoutHandler) ServeHTTP(w ResponseWriter, r *Request) {ctx := h.testContextif ctx == nil {var cancelCtx context.CancelFuncctx, cancelCtx = context.WithTimeout(r.Context(), h.dt)defer cancelCtx()}r = r.WithContext(ctx)// *Snipped*}
上面例子中 , 我们通过调用 context.WithTimeout 来使用请求上下文 。 超时值 h.dt (TimeoutHandler 的第二个参数)设置给了上下文 。 返回的上下文是请求上下文设置了超时值后的一份拷贝 。 随后 , 它作为请求上下文传给r.WithContext(ctx) 。
context.WithTimeout 方法执行了上下文取消 。 它返回了 Context 设置了一个超时值之后的副本 。 当到达超时时间后 , 就取消上下文 。
- 看不上|为什么还有用户看不上华为Mate40系列来看看内行人怎么说
- 采用|消息称一加9系列将推出三款新机,新增一加9E
- 会员|美容院使用会员管理软件给顾客更好的消费体验!
- 行业|现在行业内客服托管费用是怎么算的
- 闲鱼|电诉宝:“闲鱼”网络欺诈成用户投诉热点 Q3获“不建议下单”评级
- 美国|英国媒体惊叹:165个国家采用北斗将GPS替代,连美国也不例外?
- 桌面|日常使用的软件及网站分享 篇一:几个动态壁纸软件和静态壁纸网站:助你美化你的桌面
- 同轴心配合|用SolidWorks画一个直角传动,画四个零件就行
- 先别|用了周冬雨的照片,我会成为下一个被告?自媒体创作者先别自乱阵脚
- 速度|华为P50Pro或采用很吓人的拍照技术:液体镜头让对焦速度更快