golang 官方博文对context
的主要设计目的和实践作阐述, 本文结合该博文对服务器端实现超时的方法再作汇总。
nginx
或后端web server
配置实现请求超时 router.HandleFunc("/lazy", lazyHandler)
server := &http.Server{
Addr:"0.0.0.0:8080",
Handler: router,
ReadTimeout: time.Second * 3,
WriteTimeout: time.Second *2,
}
若web server
的handler
处理时间是5s, 此时WriteTimeout
为2s
,此时客户端会接收到err_empty_response
响应。
func lazyHandler(resp http.ResponseWriter, req *http.Request){
time.Sleep(time.Duration(5) * time.Second)
resp.Write([]byte("Lazy reply after 5 sec"))
}
在复杂的业务背景下,若处理不当该线程可能会在服务端后台形成内存泄露,可通过/debug/pprof
查看服务端goroutine
数目,显示结果是约5s后服务端对应的goroutine
会结束,且被回收。
golang context
实现请求超时控制和goroutine
生命周期控制下例中:lazyHander
负责请求参数处理和context
的初始化:
客户端发送超时参数,采用WithTimeout
来控制context
在指定时间内关闭其管道,若所有子进程中均声明了正确的context
信号接收处理,则lazyHandler
派生的所有子进程会返回并被回收。
若客户端不发送超时参数,则采用WithCancel
函数来控制所有子线程的生命周期。若所有子进程中均声明了正确的context信
号接收处理,一旦lazyHandler
的defer cancel
语句被执行,则由lazyHandler
派生的所有子进程会返回并被回收。
func lazyHandler(resp http.ResponseWriter, req *http.Request) {
var (
ctx context.Context
cancel context.CancelFunc
)
query, err := url.ParseQuery(req.URL.RawQuery)
if err != nil{
log.Fatalln("Http error")
}
keys, ok := query["timeout"]
if !ok {
ctx, cancel = context.WithCancel(context.Background())
} else {
timeout, err := time.ParseDuration(keys[0])
if err != nil {
ctx, cancel = context.WithCancel(context.Background())
} else {
ctx, cancel = context.WithTimeout(context.Background(), timeout)
}
}
defer cancel()
s := funcA(ctx)
resp.Write([]byte(s))
}
funcA
是handler
的主要业务逻辑,包含异步操作。
func funcA(ctx context.Context) string{
c := make(chan error, 1)
go func() {
c <- funcB(ctx, func()error {
time.Sleep(time.Duration(5) * time.Second)
return nil
})
}()
select {
case <-ctx.Done():
err:= <-c
return ctx.Err().Error() +"; " + err.Error()
case <-c:
return "Lazy reply after 5 sec"
}
}
这里注意,虽然ctx.Done()
信号已被接收,并不意味这该函数能马上返回,因为若该函数涉及到子线程funcB
的调用,则需要等待子线程返回,否则子线程会失去控制且可能引起内存泄露。
func funcB(ctx context.Context, f func() error) error {
c := make(chan error ,1)
go func(){
c <- f()
}()
select {
case <-ctx.Done():
return errors.New("Interrupt by context")
case err:= <-c:
return err
}
}
子线程funcB
同样能接收parent context
的ctx.Done()
,能在timeout
指定时间内强制返回,或正常执行到程序段结束。另外,lazyHandler
的defer cancel()
也能确保funcB
总能结束。
若funcA
中涉及到服务之间的调用,即调用某api
的endpoint
, 也可以将context
存于request
中,利用request
接口来实现请求超时。
func funcA(ctx context.Context) string {
c := make(chan string, 1)
go func() {
c <- func(ctx context.Context) string{
req, err := http.NewRequest("GET", "http://localhost:8079/", nil)
if err != nil {
return err.Error()
}
req = req.WithContext(ctx)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err.Error()
}else{
data, _ := ioutil.ReadAll(resp.Body)
resp.Body.Close()
return string(data)
}
}(ctx)
}()
select {
case <-ctx.Done():
err := <-c
return ctx.Err().Error() + "; " + err
case str:= <-c:
return str
}
}
注意, 这里使用了req = req.WithContext(ctx)
, 使得context
传到req
对象中,实现类似funcB
中的子线程控制。
Middleware
传入带有timeout
的context
, 参见开发者可以更优雅地通过中间件的形式设置timeout
, 另外必须在handler
实现中使用select
监听ctx.Done()
信号, 或将该ctx
交由支持ctx
作为参数的接口方法处理, 如:
rpcResponse, err := grpcFuncFoo(ctx, ...)
此方法与上方法原理上相同。