Golang context
Context背景
随着微服务的发展,大家在写服务的各种响应函数的时候,一定都是将Context作为第一个参数, 很多情况下是为了进行全链路追踪,但是Context的作用不仅这一点。
Context在Go 1.7之前,还不是在Go的标准库中的,而是在golang.org/x/net/context
,
后来Golang团队发现这包挺好使,在各个地方都能用,于是就收编进了标准库中。
Context常用在以下场景:
- 超时控制
- 上下文控制
- 在多个gourotine之间交互数据
Context结构
Context的定义如下,省略部分注释。
type Context interface {
// Deadline returns the time when work done on behalf of this context
// should be canceled. Deadline returns ok==false when no deadline is
// set. Successive calls to Deadline return the same results.
Deadline() (deadline time.Time, ok bool)
// Done returns a channel that's closed when work done on behalf of this
// context should be canceled. Done may return nil if this context can
// never be canceled. Successive calls to Done return the same value.
// The close of the Done channel may happen asynchronously,
// after the cancel function returns.
// ...
Done() <-chan struct{}
// If Done is not yet closed, Err returns nil.
// If Done is closed, Err returns a non-nil error explaining why:
// Canceled if the context was canceled
// or DeadlineExceeded if the context's deadline passed.
// After Err returns a non-nil error, successive calls to Err return the same error.
// ...
Err() error
// Value returns the value associated with this context for key, or nil
// if no value is associated with key. Successive calls to Value with
// the same key returns the same result.
//
// Use context values only for request-scoped data that transits
// processes and API boundaries, not for passing optional parameters to
// functions.
// ...
Value(key interface{}) interface{}
}
各个接口的作用如下:
接口 | 作用 |
---|---|
Deadline | 返回一个time.Time, 表示当前Context应该结束的时间。如果ok为false,则没有结束时间 |
Done | 返回一个空结构的Channel,如果工作该结束了,则该channel会被关闭。如果nil则该context永远不会被cancel |
Err | 返回conext被取消的原因。当Err返回不为空时,则以后每次调用都是同样的结果 |
Value | 返回创建context时绑定的数据,可以实现协程间的数据共享 |
Context是可以传入给多个goroutine的,他是协程安全的。
同时,包中还定义了提供cancel功能所需要实现的接口。这个是cancelCtx和timerCtx实现的接口。
// A canceler is a context type that can be canceled directly. The
// implementations are *cancelCtx and *timerCtx.
type canceler interface {
cancel(removeFromParent bool, err error)
Done() <-chan struct{}
}
在标准库中,提供了4个Context的实现,可以来参考使用
实现 | 结构体 | 作用 |
---|---|---|
emptyCtx | type emptyCtx int | 完全是空的Context,所有的方法返回都是nil |
cancelCtx | type cancelCtx struct { Context mu sync.Mutex done chan struct{} children map[canceler]struct{} err error } |
继承自Context,同时也实现了canceler接口 |
timerCtx | type timerCtx struct { cancelCtx timer *time.Timer } |
继承自cancelCtx,增加了timeout机制 |
valueCtx | type valueCtx struct { Context key, val interface{} } |
集成子Context,提供存储键值对数据的功能 |
Context的创建
为了更方便的创建Context,包里头定义了Background来作为所有Context的根,它是一个emptyCtx的实例。
var (
background = new(emptyCtx)
todo = new(emptyCtx)
)
func Background() Context {
return background
}
你可以认为所有的Context是树的结构,Background是树的根,当任一Context被取消的时候,那么继承它的Context都将被回收。
Context使用举例
WithCancel
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
c := newCancelCtx(parent)
propagateCancel(parent, &c)
return &c, func() { c.cancel(true, Canceled) }
}
WithCancel会返回两个返回值,除了返回一个Context外,还会返回一个CancelFunc,用来cancel掉相应的goroutine。
package main
import (
"context"
"fmt"
)
func main() {
// gen generates integers in a separate goroutine and
// sends them to the returned channel.
// The callers of gen need to cancel the context once
// they are done consuming generated integers not to leak
// the internal goroutine started by gen.
gen := func(ctx context.Context) <-chan int {
dst := make(chan int)
n := 1
go func() {
for {
select {
case <-ctx.Done():
return // returning not to leak the goroutine
case dst <- n:
n++
}
}
}()
return dst
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel() // cancel when we are finished consuming integers
for n := range gen(ctx) {
fmt.Println(n)
if n == 5 {
break
}
}
}
Output:
1
2
3
4
5
WithDeadline / WithTimeout
WithDeadline和WithTimeout差不多,Deadline接收的是一个指定的时间点,比如16:00,过了这个时间点后,就会cancel掉执行的goroutine, 而Timeout则是接受一个指定的时间值,比如5秒,执行了这么久过后,就会cancel相应的goroutine。
package main
import (
"context"
"fmt"
"time"
)
func main() {
d := time.Now().Add(50 * time.Millisecond)
ctx, cancel := context.WithDeadline(context.Background(), d)
// Pass a context with a timeout to tell a blocking function that it
// should abandon its work after the timeout elapses.
// ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
// Even though ctx will be expired, it is good practice to call its
// cancellation function in any case. Failure to do so may keep the
// context and its parent alive longer than necessary.
defer cancel()
select {
case <-time.After(1 * time.Second):
fmt.Println("overslept")
case <-ctx.Done():
fmt.Println(ctx.Err()) // prints "context deadline exceeded"
}
}
output:
context deadline exceeded
WithValue
如果执行时需要携带关键信息,为全链路提供线索,比如接入elk等系统,需要来一个trace_id,那WithValue就非常适合做这个事。
package main
import (
"context"
"fmt"
)
func main() {
type favContextKey string
f := func(ctx context.Context, k favContextKey) {
if v := ctx.Value(k); v != nil {
fmt.Println("found value:", v)
return
}
fmt.Println("key not found:", k)
}
k := favContextKey("language")
ctx := context.WithValue(context.Background(), k, "Go")
f(ctx, k)
f(ctx, favContextKey("color"))
}
output:
found value: Go
key not found: color
Context使用注意事项
- 在传入Context的时候,不要传入nil,如果拿不准要传入什么,那就传入context.TODO()
- Context作为全局链路跟踪的时候,要作为第一个参数,且名字按照惯例写为
ctx