1. context简介
contextgoroutine
WithCancelWithDeadlineWithTimeoutWithValue
简而言之:context的作用就是一种多Goroutine管理机制。
contextcontext
context包的主要内容可以被概括为1个接口,4个具体实现,6个函数:
2. context接口
context
// A Context carries a deadline, cancelation signal, and request-scoped values
// across API boundaries. Its methods are safe for simultaneous use by multiple
// goroutines.
type Context interface {
// Done returns a channel that is closed when this Context is canceled
// or times out.
Done() <-chan struct{}
// Err indicates why this context was canceled, after the Done channel
// is closed.
Err() error
// Deadline returns the time when this Context will be canceled, if any.
Deadline() (deadline time.Time, ok bool)
// Value returns the value associated with key or nil if none.
Value(key interface{}) interface{}
}
DeadlineDoneErrValue
3. emptyCtx、cancelCtx、timerCtx和valueCtx
emptyCtx
// An emptyCtx is never canceled, has no values, and has no deadline. It is not
// struct{}, since vars of this type must have distinct addresses.
type emptyCtx int
emptyCtx
cancelCtx
// A cancelCtx can be canceled. When canceled, it also cancels any children
// that implement canceler.
type cancelCtx struct {
Context
mu sync.Mutex // protects following fields
done atomic.Value // of chan struct{}, created lazily, closed by first cancel call
children map[canceler]struct{} // set to nil by the first cancel call
err error // set to non-nil by the first cancel call
}
cancelCtxcontextdonechildrenerr
timerCtx
// A timerCtx carries a timer and a deadline. It embeds a cancelCtx to
// implement Done and Err. It implements cancel by stopping its timer then
// delegating to cancelCtx.cancel.
type timerCtx struct {
cancelCtx
timer *time.Timer // Under cancelCtx.mu.
deadline time.Time
}
timerCtx
timerCtxtimerCtx
valueCtx
// A valueCtx carries a key-value pair. It implements Value for that key and
// delegates all other calls to the embedded Context.
type valueCtx struct {
Context
key, val interface{}
}
valueCtxvalueCtxvalueCtx
4. Background() 和 TODO()
对于goroutine,他们的创建和调用关系总是像层层调用进行的,就像一个树状结构,而更靠顶部的context应该有办法主动关闭下属的goroutine的执行。为了实现这种关系,context也是一个树状结构,叶子节点总是由根节点衍生出来的。
要创建context树,第一步应该得到根节点,context.Background()函数的返回值就是根节点。
var (
background = new(emptyCtx)
todo = new(emptyCtx)
)
// 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.
func Background() Context {
return background
}
// TODO returns a non-nil, empty Context. Code should use context.TODO when
// it's unclear which Context to use or it is not yet available (because the
// surrounding function has not yet been extended to accept a Context
// parameter).
func TODO() Context {
return todo
}
这两个函数函数内部都会创建一个emptyCtx。
Background()
Background()函数返回的变量结构:
TODO()
5. WithCancel()、WithDeadline()、WithTimeout()和WithValue()
有了根节点,就可以创建子孙节点了,context包提供了一系列方法来创建他们:
// A CancelFunc tells an operation to abandon its work.
// A CancelFunc does not wait for the work to stop.
// A CancelFunc may be called by multiple goroutines simultaneously.
// After the first call, subsequent calls to a CancelFunc do nothing.
type CancelFunc func()
怎么通过context传递改变后的状态呢?
在父goroutine中可以通过Withxx方法获取一个cancel方法,从而获得了操作子context的权力。
WithCancel()
// WithCancel returns a copy of parent with a new Done channel. The returned
// context's Done channel is closed when the returned cancel function is called
// or when the parent context's Done channel is closed, whichever happens first.
//
// Canceling this context releases resources associated with it, so code should
// call cancel as soon as the operations running in this Context complete.
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
if parent == nil {
panic("cannot create context from nil parent")
}
c := newCancelCtx(parent)
propagateCancel(parent, &c)
return &c, func() { c.cancel(true, Canceled) }
}
WithCancel
我们来了解一个利用context结束goroutine的demo。
package main
import (
"context"
"fmt"
"time"
)
/*
创建一个通道chan,启动goroutine
for循环储存数据
*/
func gen(ctx context.Context) <-chan int {
dst := make(chan int)
n := 1
go func() {
for {
select {
case <- ctx.Done() :
return //return结束该goroutine,防止泄露
case dst <- n:
n++
}
}
}()
return dst
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
//当取数据n == 5的时候,执行defer cancel操作
defer cancel()
for n := range gen(ctx) {
fmt.Println(n)
if n == 5 {
break
}
}
time.Sleep(time.Second * 5)
}
运行结果:
1
2
3
4
5
gengen
WithDeadline()
// WithDeadline returns a copy of the parent context with the deadline adjusted
// to be no later than d. If the parent's deadline is already earlier than d,
// WithDeadline(parent, d) is semantically equivalent to parent. The returned
// context's Done channel is closed when the deadline expires, when the returned
// cancel function is called, or when the parent context's Done channel is
// closed, whichever happens first.
//
// Canceling this context releases resources associated with it, so code should
// call cancel as soon as the operations running in this Context complete.
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
if parent == nil {
panic("cannot create context from nil parent")
}
if cur, ok := parent.Deadline(); ok && cur.Before(d) {
// The current deadline is already sooner than the new one.
return WithCancel(parent)
}
c := &timerCtx{
cancelCtx: newCancelCtx(parent),
deadline: d,
}
propagateCancel(parent, c)
dur := time.Until(d)
if dur <= 0 {
c.cancel(true, DeadlineExceeded) // deadline has already passed
return c, func() { c.cancel(false, Canceled) }
}
c.mu.Lock()
defer c.mu.Unlock()
if c.err == nil {
c.timer = time.AfterFunc(dur, func() {
c.cancel(true, DeadlineExceeded)
})
}
return c, func() { c.cancel(true, Canceled) }
}
WithDeadline
package main
import (
"context"
"fmt"
"time"
)
func main() {
d := time.Now().Add(50 * time.Millisecond)
ctx, cancel := context.WithDeadline(context.Background(), d)
// 尽管ctx会过期,但在任何情况下调用它的cancel函数都是很好的实践。
// 如果不这样做,可能会使上下文及其父类存活的时间超过必要的时间。
defer cancel()
select {
case <- time.After(1 * time.Second):
fmt.Println("overslept")
case <- ctx.Done():
fmt.Println(ctx.Err())
}
}
运行结果:
context deadline exceeded
deadlinecontext.WithDeadline(context.Background(), d)selectoverslept
ctx.Done()contextctx.Err()
WithTimeout
// WithTimeout returns WithDeadline(parent, time.Now().Add(timeout)).
//
// Canceling this context releases resources associated with it, so code should
// call cancel as soon as the operations running in this Context complete:
//
// func slowOperationWithTimeout(ctx context.Context) (Result, error) {
// ctx, cancel := context.WithTimeout(ctx, 100*time.Millisecond)
// defer cancel() // releases resources if slowOperation completes before timeout elapses
// return slowOperation(ctx)
// }
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
return WithDeadline(parent, time.Now().Add(timeout))
}
WithTimeout
当顶层的Request请求函数结束后,我们就可以cancel掉某个context,从而层层Goroutine根据判断cxt.Done()来结束。
package main
import (
"context"
"fmt"
"sync"
"time"
)
var wg sync.WaitGroup
func worker(ctx context.Context) {
LOOP:
for {
fmt.Println("db connecting ...")
time.Sleep(time.Millisecond * 10) //假设正常连接数据库耗时10毫秒
select {
case <- ctx.Done(): //50毫秒后自动调用
break LOOP
default:
}
}
fmt.Println("worker done!")
wg.Done()
}
func main() {
//设置一个50毫秒的超时
ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond * 50)
wg.Add(1)
go worker(ctx)
time.Sleep(time.Second * 5)
cancel() //通知子goroutine结束
wg.Wait()
fmt.Println("over")
}
运行结果:
db connecting ...
db connecting ...
db connecting ...
db connecting ...
db connecting ...
worker done!
over
WithValue()
// WithValue returns a copy of parent in which the value associated with key is
// val.
//
// Use context Values only for request-scoped data that transits processes and
// APIs, not for passing optional parameters to functions.
//
// The provided key must be comparable and should not be of type
// string or any other built-in type to avoid collisions between
// packages using context. Users of WithValue should define their own
// types for keys. To avoid allocating when assigning to an
// interface{}, context keys often have concrete type
// struct{}. Alternatively, exported context key variables' static
// type should be a pointer or interface.
func WithValue(parent Context, key, val interface{}) Context {
if parent == nil {
panic("cannot create context from nil parent")
}
if key == nil {
panic("nil key")
}
if !reflectlite.TypeOf(key).Comparable() {
panic("key is not comparable")
}
return &valueCtx{parent, key, val}
}
WithValue
package main
import (
"context"
"fmt"
"sync"
"time"
)
type TraceCode string
var wg sync.WaitGroup
func worker(ctx context.Context) {
key := TraceCode("TRACE_CODE")
traceCode, ok := ctx.Value(key).(string) //在子goroutine中获取trace code
if !ok {
fmt.Println("invalid trace code")
}
LOOP:
for {
fmt.Printf("worker, trace code:%s\n", traceCode)
time.Sleep(time.Millisecond * 10) // 假设正常连接数据库耗时10毫秒
select {
case <-ctx.Done(): // 50毫秒后自动调用
break LOOP
default:
}
}
fmt.Println("worker done!")
wg.Done()
}
func main() {
// 设置一个50毫秒的超时
ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*50)
// 在系统的入口中设置trace code传递给后续启动的goroutine实现日志数据聚合
ctx = context.WithValue(ctx, TraceCode("TRACE_CODE"), "12512312234")
wg.Add(1)
go worker(ctx)
time.Sleep(time.Second * 5)
cancel() // 通知子goroutine结束
wg.Wait()
fmt.Println("over")
}
运行结果:
worker, trace code:12512312234
worker, trace code:12512312234
worker, trace code:12512312234
worker, trace code:12512312234
worker, trace code:12512312234
worker done!
over
6. 使用context小结
小结:
- context包通过构建树形关系的context,来达到上一层goroutine对下一层goroutine的控制。对于处理一个request请求操作,需要通过goroutine来层层控制goroutine,以及传递一些变量来共享。
- context变量的请求周期一般为一个请求的处理周期。即针对一个请求创建context对象;在请求处理结束后,撤销此ctx变量,释放资源。
- 每创建一个goroutine,要不将原有context传递给子goroutine,要么创建一个子context传递给goroutine.
- Context能灵活地存储不同类型、不同数目的值,并且使多个Goroutine安全地读写其中的值。
- 当通过父
Context对象创建子Context时,可以同时获得子Context的撤销函数,这样父goroutine就获得了子goroutine的撤销权。
注意事项:
- 推荐以参数的方式显示传递Context。
- 以Context作为参数的函数方法,应该把Context作为第一个参数。
- 给一个函数方法传递Context的时候,不要传递nil,如果不知道传递什么,就使用context.TODO()。
- Context的Value相关方法应该传递请求域的必要数据,不应该用于传递可选参数。
- Context是线程安全的,可以放心的在多个goroutine中传递。