golang之context
context本义是上下文,作用有二,主要用于控制子任务(goroutine)的生命周期,即同步结束子任务,本质是一种协程调度方式。其次用于父子任务传递变量、取消信号和deadlines。
使用context
时有两点值得注意:上游任务仅仅使用context通知下游任务不再需要,但不会直接干涉和中断下游任务的执行,由下游任务自行决定后续的处理操作,也就是说ontext的取消操作是无侵入的;context是线程安全的,因为context本身是不可变的(immutable),因此可以放心地在多个协程中传递使用。
一、context包
1. context自1.7版本引入,专门用来简化 对于处理单个请求的多个 goroutine 之间与请求域的数据、取消信号、截止时间等相关操作,这些操作可能涉及多个 API 调用。A Context carries a deadline, a cancellation signal, and other values across API boundaries.
type Context interface { Deadline() (deadline time.Time, ok bool) Done() <-chan struct{} Err() error Value(key interface{}) interface{} }
Deadline
返回绑定当前context
的任务被取消的截止时间;如果没有设定期限,将返回ok == false
。
Done
当绑定当前context
的任务被取消时,将返回一个关闭的channel
;如果当前context
不会被取消,将返回nil
。
Err
如果Done
返回的channel
没有关闭,将返回nil
;如果Done
返回的channel
已经关闭,将返回非空的值表示任务结束的原因。如果是context
被取消,Err
将返回Canceled
;如果是context
超时,Err
将返回DeadlineExceeded
。
Value
返回context
存储的键值对中当前key
对应的值,如果没有对应的key
,则返回nil
。
2. 常用的顶层Context一般用Background()返回。
func Background() Context
func TODO() Context
Background returns a non-nil, empty Context. It is never canceled, has no values, and has no deadline. It is typically used by the main function, initialization, and tests, and as the top-level Context for incoming requests.
3. The WithCancel, WithDeadline, and WithTimeout functions take a Context (the parent) and return a derived Context (the child) and a CancelFunc.
type CancelFunc func()
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
4. Use context Values only for request-scoped data that transits processes and APIs, not for passing optional parameters to functions.
func WithValue(parent Context, key, val interface{}) Context
WithValue returns a copy of parent in which the value associated with key is val. 添加键值对不是在原context
结构体上直接添加,而是以此context
作为父节点,重新创建一个新的valueCtx
子节点,将键值对添加在子节点上,由此形成一条context
链。获取value
的过程就是在这条context
链上由尾部上前搜寻:
key必须是可比较的,为了避免冲突,key不应该是string或其他任何内置类型,应该定义新的类型。
type User struct {...} type key int var userKey key func NewContext(ctx context.Context, u *User) context.Context{ return context.WithValue(ctx, userKey, u) } func FromContext(ctx context.Context) (*User, bool){ u, ok := ctx.Value(userKey).(*User) return u, ok }
5. 推荐应用:不应该在struct中存储Contexts,而应该在每个函数中将context作为第一个参数显示的传递,典型的命名为ctx:
func DoSomething(ctx context.Context, arg Arg) error { // ... use ctx ... }
二、示例
调用服务端API时如何在客户端实现超时控制?
package main import ( "fmt" "math/rand" "net/http" "time" ) // server端,随机出现慢响应 func indexHandler(w http.ResponseWriter, r *http.Request) { number := rand.Intn(2) if number == 0 { time.Sleep(time.Second * 10) // 耗时10秒的慢响应 fmt.Fprintf(w, "slow response") return } fmt.Fprint(w, "quick response") } func main() { http.HandleFunc("/", indexHandler) err := http.ListenAndServe(":8000", nil) if err != nil { panic(err) } }
// context_timeout/client/main.go package main import ( "context" "fmt" "io/ioutil" "net/http" "sync" "time" ) // 客户端 type respData struct { resp *http.Response err error } func doCall(ctx context.Context) { transport := http.Transport{ // 请求频繁可定义全局的client对象并启用长链接 // 请求不频繁使用短链接 DisableKeepAlives: true, } client := http.Client{ Transport: &transport, } respChan := make(chan *respData, 1) req, err := http.NewRequest("GET", "http://127.0.0.1:8000/", nil) if err != nil { fmt.Printf("new requestg failed, err:%v\n", err) return } req = req.WithContext(ctx) // 使用带超时的ctx创建一个新的client request var wg sync.WaitGroup wg.Add(1) defer wg.Wait() go func() { resp, err := client.Do(req) fmt.Printf("client.do resp:%v, err:%v\n", resp, err) rd := &respData{ resp: resp, err: err, } respChan <- rd wg.Done() }() select { case <-ctx.Done(): //transport.CancelRequest(req) fmt.Println("call api timeout") case result := <-respChan: fmt.Println("call server api success") if result.err != nil { fmt.Printf("call server api failed, err:%v\n", result.err) return } defer result.resp.Body.Close() data, _ := ioutil.ReadAll(result.resp.Body) fmt.Printf("resp:%v\n", string(data)) } } func main() { // 定义一个100毫秒的超时 ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*100) defer cancel() // 调用cancel释放子goroutine资源 doCall(ctx) }
参考: