golang 官方博文對context
的主要設計目的和實踐作闡述, 本文結合該博文對服務器端實現超時的方法再作彙總。
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
會結束,且被回收。
下例中: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, ...)
此方法與上方法原理上相同。