context简单入门

在开发高效且可维护的 Go 应用程序时,处理超时、取消操作和传递请求范围的数据变得至关重要。

这时,Go 标准库中的 context 包就显得尤其重要了,它提供了在不同 API 层级之间传递取消信号、超时时间、截止日期,以及其他特定请求的值的能力。

简介

context

context 包允许你传递可取消的信号、超时时间、截止日期,以及跨 API 边界的请求范围的数据。在并发编程中,它非常有用,尤其是处理那些可能需要提前停止的长时间运行操作。

特性

Go 语言的 context 包提供了一系列方法,用于创建上下文(Context),这些上下文可以帮助我们管理请求的生命周期、取消信号、超时、截止日期和传递请求范围的数据。

官方建议将context 作为第一个参数,并且不断的透传下去,这样的效果就是整个项目中到处都是context。但是仅仅使用透传将context 传递下去并不能做到,取消信号,超时等功能,context 本身的机制采用的是通知机制,简单的透传并不能起作用。

1
2
3
4
5
6
7
8
9
10
11
12
13
func main()  {
ctx,cancel := context.WithTimeout(context.Background(),10 * time.Second)
defer cancel()
go Monitor(ctx)

time.Sleep(20 * time.Second)
}

func Monitor(ctx context.Context) {
for {
fmt.Print("monitor")
}
}

在上面的例子中,虽然context 进行了透传,但是没有正确的使用就不会起作用

使用

父context

context 本身就是一个interface{}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
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.
//
// WithCancel arranges for Done to be closed when cancel is called;
// WithDeadline arranges for Done to be closed when the deadline
// expires; WithTimeout arranges for Done to be closed when the timeout
// elapses.
//
// Done is provided for use in select statements:
//
// // Stream generates values with DoSomething and sends them to out
// // until DoSomething returns an error or ctx.Done is closed.
// func Stream(ctx context.Context, out chan<- Value) error {
// for {
// v, err := DoSomething(ctx)
// if err != nil {
// return err
// }
// select {
// case <-ctx.Done():
// return ctx.Err()
// case out <- v:
// }
// }
// }
//
// See https://blog.golang.org/pipelines for more examples of how to use
// a Done channel for cancellation.
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.
//
// A key identifies a specific value in a Context. Functions that wish
// to store values in Context typically allocate a key in a global
// variable then use that key as the argument to context.WithValue and
// Context.Value. A key can be any type that supports equality;
// packages should define keys as an unexported type to avoid
// collisions.
//
// Packages that define a Context key should provide type-safe accessors
// for the values stored using that key:
//
// // Package user defines a User type that's stored in Contexts.
// package user
//
// import "context"
//
// // User is the type of value stored in the Contexts.
// type User struct {...}
//
// // key is an unexported type for keys defined in this package.
// // This prevents collisions with keys defined in other packages.
// type key int
//
// // userKey is the key for user.User values in Contexts. It is
// // unexported; clients use user.NewContext and user.FromContext
// // instead of using this key directly.
// var userKey key
//
// // NewContext returns a new Context that carries value u.
// func NewContext(ctx context.Context, u *User) context.Context {
// return context.WithValue(ctx, userKey, u)
// }
//
// // FromContext returns the User value stored in ctx, if any.
// func FromContext(ctx context.Context) (*User, bool) {
// u, ok := ctx.Value(userKey).(*User)
// return u, ok
// }
Value(key any) any
}

Deadline

简单理解为,过期时间是什么时候,通过这个方法,就可以获取停止的时间

Done

done 本身是一个阻塞的channel

当已经完成了context 阻塞就会消失

Err

如果Done尚未关闭,Err返回nil。
如果Done关闭,Err将返回一个非零错误,解释原因:
如果上下文被取消,则取消
如果上下文的截止日期已过,则为DeadlineExceeded。
在Err返回非零错误之后,对Err的连续调用将返回相同的错误。

Value

可以用于取值,可以 简单的理解

这个Value 可以用于取最下层的context 的值,如果被设置好了

创建context 的方式有两个函数

context.TODOcontext.Backgroud

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// 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
}

// 下方代码从源码删减得到,只为更加直观展示对应的变量
var (
background = new(emptyCtx)
)

type emptyCtx int

func (*emptyCtx) Deadline() (deadline time.Time, ok bool) {
return
}

func (*emptyCtx) Done() <-chan struct{} {
return nil
}

func (*emptyCtx) Err() error {
return nil
}

func (*emptyCtx) Value(key any) any {
return nil
}

func (e *emptyCtx) String() string {
switch e {
case background:
return "context.Background"
case todo:
return "context.TODO"
}
return "unknown empty Context"
}

可以发现使用Backgroud 创建的channel 是最顶层的channel ,不会被取消,没有值,也不会deadline。通过观察源码发现,所谓的channel 其实就是一个空context

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// 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
}

// 下方代码从源码删减得到,只为更加直观展示对应的变量
var (
todo = new(emptyCtx)
)

type emptyCtx int

func (*emptyCtx) Deadline() (deadline time.Time, ok bool) {
return
}

func (*emptyCtx) Done() <-chan struct{} {
return nil
}

func (*emptyCtx) Err() error {
return nil
}

func (*emptyCtx) Value(key any) any {
return nil
}

func (e *emptyCtx) String() string {
switch e {
case background:
return "context.Background"
case todo:
return "context.TODO"
}
return "unknown empty Context"
}

可以发现,todo 和backgroud 创建的context 都是空context

上面的两种方式是创建根context,不具备任何功能,所以在此称之为父context

子context

可以使用下面的方法创建具有对应功能的context

1
2
3
4
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
func WithValue(parent Context, key, val interface{}) Context

这些就具有对应的功能

并且使用这些方法,创建的context 可以认为是父context 的一个衍生,当创建的数量够多了,就形成了一个context 树

img

$图片取自【Go语言】小白也能看懂的context包详解:从入门到精通 - 个人文章 - SegmentFault 思否$

并且,我们可以依赖于我们创建的子节点创建新的context

WithValue携带数据

我们日常在业务开发中都希望能有一个trace_id能串联所有的日志,这就需要我们打印日志时能够获取到这个trace_id,在python中我们可以用gevent.local来传递,在java中我们可以用ThreadLocal来传递,在Go语言中我们就可以使用Context来传递,通过使用WithValue来创建一个携带trace_idcontext,然后不断透传下去,打印日志时输出即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 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 any) 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}
}

可以查看下面的这个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
const (
KEY = "trace_id"
)

func NewRequestID() string {
return strings.Replace(uuid.New().String(), "-", "", -1)
}

func NewContextWithTraceID() context.Context {
ctx := context.WithValue(context.Background(), KEY, NewRequestID())
return ctx
}

func PrintLog(ctx context.Context, message string) {
fmt.Printf("%s|info|trace_id=%s|%s", time.Now().Format("2006-01-02 15:04:05"), GetContextValue(ctx, KEY), message)
}

func GetContextValue(ctx context.Context, k string) string {
v, ok := ctx.Value(k).(string)
if !ok {
return ""
}
return v
}

func ProcessEnter(ctx context.Context) {
PrintLog(ctx, "Golang梦工厂")
}

func main() {
ProcessEnter(NewContextWithTraceID())
}

我们基于context.Background创建一个携带trace_idctx,然后通过context树一起传递,从中派生的任何context都会获取此值,我们最后打印日志的时候就可以从ctx中取值输出到日志中。目前一些RPC框架都是支持了Context,所以trace_id的向下传递就更方便了。

使用WithValue 的注意事项

  • 不建议使用context值传递关键参数,关键参数应该显示的声明出来,不应该隐式处理,context中最好是携带签名、trace_id这类值。
  • 因为携带value也是keyvalue的形式,为了避免context因多个包同时使用context而带来冲突,key建议采用内置类型。
  • 上面的例子我们获取trace_id是直接从当前ctx获取的,实际我们也可以获取父context中的value,在获取键值对是,我们先从当前context中查找,没有找到会在从父context中查找该键对应的值直到在某个父context中返回 nil 或者查找到对应的值。
  • context传递的数据中keyvalue都是interface类型,这种类型编译期无法确定类型,所以不是很安全,所以在类型断言时别忘了保证程序的健壮性。

超时控制

通常健壮的程序都是要设置超时时间的,避免因为服务端长时间响应消耗资源,所以一些web框架或rpc框架都会采用withTimeout或者withDeadline来做超时控制,当一次请求到达我们设置的超时时间,就会及时取消,不在往下执行。withTimeoutwithDeadline作用是一样的,就是传递的时间参数不同而已,他们都会通过传入的时间来自动取消Context,这里要注意的是他们都会返回一个cancelFunc方法,通过调用这个方法可以达到提前进行取消,不过在使用的过程还是建议在自动取消后也调用cancelFunc去停止定时减少不必要的资源浪费。

withTimeoutWithDeadline不同在于WithTimeout将持续时间作为参数输入而不是时间对象,这两个方法使用哪个都是一样的,看业务场景和个人习惯了,因为本质withTimout内部也是调用的WithDeadline

现在我们就举个例子来试用一下超时控制

  1. 当超过时间就终止下面的执行

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    func newContextWithTimeOutForOver() (context.Context, context.CancelFunc) {
    return context.WithTimeout(context.Background(), 4*time.Second)
    }

    func dealForOver(ctx context.Context) {
    for i := 0; i < 10; i++ {
    time.Sleep(1 * time.Second)
    select {
    case <-ctx.Done():
    fmt.Println(ctx.Err())
    return
    default:
    fmt.Println("deal time is:", i)
    }
    }
    }

    func handlerForOver() {
    ctx, cancelFunc := newContextWithTimeOutForOver()
    defer cancelFunc()
    dealForOver(ctx)
    }

    func TestForOver() {
    handlerForOver()
    }
  2. 没有超过时间,但是手动终止

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    func newContextWithTimeOutForNoOver() (context.Context, context.CancelFunc) {
    return context.WithTimeout(context.Background(), 4*time.Second)
    }

    func dealForNoOver(ctx context.Context, cancelFunc context.CancelFunc) {
    for i := 0; i < 10; i++ {
    time.Sleep(1 * time.Second)
    select {
    case <-ctx.Done():
    fmt.Println(ctx.Err())
    return
    default:
    fmt.Println("deal time is:", i+1)
    cancelFunc()
    }
    }
    }

    func handlerForNoOver() {
    ctx, cancelFunc := newContextWithTimeOutForNoOver()
    defer cancelFunc()
    dealForNoOver(ctx, cancelFunc)
    }

    func TestForNoOver() {
    handlerForNoOver()
    }

注意

总结

context包主要提供了两种方式创建context:

  • context.Backgroud()
  • context.TODO()

这两个函数其实只是互为别名,没有差别,官方给的定义是:

  • context.Background 是上下文的默认值,所有其他的上下文都应该从它衍生(Derived)出来。
  • context.TODO 应该只在不确定应该使用哪种上下文时使用;

所以在大多数情况下,我们都使用context.Background作为起始的上下文向下传递。

上面的两种方式是创建根context,不具备任何功能

使用起来还是比较容易的,既可以超时自动取消,又可以手动控制取消。这里大家要记的一个坑,就是我们往从请求入口透传的调用链路中的context是携带超时时间的,如果我们想在其中单独开一个goroutine去处理其他的事情并且不会随着请求结束后而被取消的话,那么传递的context要基于context.Background或者context.TODO重新衍生一个传递,否决就会和预期不符合了,可以参考context使用不当引发的一个bug (qq.com)

WithCancel取消控制

手动关闭

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func speak(ctx context.Context) {
for range time.Tick(time.Second) {
select {
case <-ctx.Done():
fmt.Println("停止输出")
return
default:
fmt.Println("balabalabala")
}
}
}

func Test() {
ctx, cancel := context.WithCancel(context.Background())
go speak(ctx)
time.Sleep(3 * time.Second)
cancel()
time.Sleep(1 * time.Second)
}

自定义context

因为Context本质是一个接口,所以我们可以通过实现Context达到自定义Context的目的,一般在实现Web框架或RPC框架往往采用这种形式,比如gin框架的Context就是自己有封装了一层

参考

【Go语言】小白也能看懂的context包详解:从入门到精通 - 个人文章 - SegmentFault 思否

Golang深入浅出之-Go语言上下文(context)包:处理取消与超时-腾讯云开发者社区-腾讯云 (tencent.com)

一文弄懂Go语言的Context包,值得收藏! - 左诗右码 - SegmentFault 思否


context简单入门
https://tsy244.github.io/2024/08/13/go/context简单入门/
Author
August Rosenberg
Posted on
August 13, 2024
Licensed under