文章目录
- 1. 简介
- 2. 常见用法
- 2.1 控制goroutine的生命周期(cancel)
- 2.2 传递超时(Timeout)信息
- 2.3 传递截止时间(Deadline)
- 2.4 传递请求范围内的全局数据 (value)
- 3 特点
- 3.1 上下文的不可变性
- 3.2 链式传递
- 3.3 超时和截止时间
- 3.4 多级取消机制
- 3.5 context.Value() 传递全局数据
- 3.6 context 线程安全
- 3.7 层次化的取消信号传播
- 4 底层实现原理
- 4.1 Context接口
- 4.2 emptyCtx类
- 4.3 cancelCtx类
- 4.4 timerCtx类
- 4.5 valueCtx类
1. 简介
在 Go 语言中,context
包主要用于在 并发编程 中控制和管理 goroutine 的生命周期。它提供了一种机制,可以通过传递 context.Context
来协调多个 goroutine,特别是在需要取消操作、超时控制和传递共享数据时。
2. 常见用法
2.1 控制goroutine的生命周期(cancel)
context
允许父 goroutine 可以通知子 goroutine 停止工作。例如,当你在 HTTP 服务器中处理一个请求时,如果客户端关闭了连接,你可能不再需要继续处理这个请求。通过 context
,你可以在主流程中取消子流程(即 goroutine),避免不必要的资源消耗。
ctx, cancel := context.WithCancel(context.Background())
go func() {select {case <-ctx.Done():// 接收到取消信号,停止工作fmt.Println("Goroutine canceled")}
}()
// 取消操作
cancel()
2.2 传递超时(Timeout)信息
context
还可以传递一个超时时间,允许操作在指定时间后自动停止。这对于需要限定时间完成的操作(如数据库查询、API 调用)非常有用。
ctx, cancel := context.WithTimeout(context.Background(), time.Second*2)
defer cancel()select {
case <-time.After(time.Second * 1):fmt.Println("Operation completed within timeout")
case <-ctx.Done():fmt.Println("Operation timed out")
}
2.3 传递截止时间(Deadline)
context
可以包含一个截止时间(Deadline
),这是在某个具体的时间点之后,所有的操作都应该停止。与超时类似,但它使用绝对时间而不是相对时间 。
deadline := time.Now().Add(time.Second * 5)
ctx, cancel := context.WithDeadline(context.Background(), deadline)
defer cancel()select {
case <-time.After(time.Second * 6):fmt.Println("Operation completed")
case <-ctx.Done():fmt.Println("Deadline exceeded")
}
2.4 传递请求范围内的全局数据 (value)
context
还可以携带一些全局数据,虽然 context
设计的初衷并不是为了数据传递,但在一些场景下,利用 context
传递与请求上下文相关的信息是很常见的做法。例如,在 Web 服务中传递用户认证信息、跟踪请求 ID 等。
package mainimport ("context""fmt"
)// 创建一个 key 类型,用于在 context 中存储和检索数据
type key stringconst userIDKey key = "userID"// 传递用户 ID 的函数
func processRequest(ctx context.Context) {// 从 context 中取出用户 IDuserID := ctx.Value(userIDKey)if userID != nil {fmt.Println("User ID found in context:", userID)} else {fmt.Println("No User ID found in context")}
}func main() {// 创建一个带有用户 ID 的 contextctx := context.WithValue(context.Background(), userIDKey, "12345")// 传递带有用户 ID 的 contextprocessRequest(ctx)// 调用时没有用户 IDprocessRequest(context.Background())
}
3 特点
3.1 上下文的不可变性
context
是不可变的。一旦创建了一个 context
,你不能修改它。每次你想要添加新的值、超时或取消机制时,都会创建一个新的子 context
。这有助于保持 context
的并发安全,确保多个 goroutine 可以共享同一个 context
而不发生数据竞争。
例如,使用 context.WithValue
添加键值对时,实际上是创建了一个新的 context
,旧的 context
保持不变。
parentCtx := context.Background()
childCtx := context.WithValue(parentCtx, "key", "value")
3.2 链式传递
context
是链式传递的。你可以从一个父 context
创建多个子 context
,并且这些子 context
可以继续创建它们的子 context
。这种设计允许你在复杂的程序中,管理多个 context
,并且可以确保每个子 context
都能够追溯到其父 context
。
例如,当你调用 context.WithCancel
或 context.WithTimeout
时,都会基于父 context
创建新的子 context
。
3.3 超时和截止时间
context
允许你为操作设置 超时时间 或 截止时间。通过 context.WithTimeout
和 context.WithDeadline
,你可以确保某些操作在指定时间内完成,否则自动取消。这对于网络请求、数据库查询等可能需要限制执行时间的操作非常有用。
context.WithTimeout
:为上下文设置相对的超时时间。例如,在 5 秒后自动取消。
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
context.WithDeadline
:为上下文设置绝对的截止时间。例如,在某个具体的时间点后取消。
deadline := time.Now().Add(10 * time.Second)
ctx, cancel := context.WithDeadline(context.Background(), deadline)
defer cancel()
在这两种情况下,一旦时间到了,ctx.Done()
通道会关闭,所有使用这个 context
的操作都会被取消。
3.4 多级取消机制
除了父 context
取消时会取消所有子 context
外,子 context
也可以通过独立的取消机制取消自己。例如,如果你为某个子 context
设置了独立的超时或调用了它的 cancel
函数,子 context
会取消,而其他的兄弟 context
或父 context
不受影响。
parentCtx, parentCancel := context.WithCancel(context.Background())
childCtx, childCancel := context.WithTimeout(parentCtx, 2*time.Second)go func() {<-childCtx.Done()fmt.Println("Child context cancelled")
}()time.Sleep(3 * time.Second)
parentCancel() // 取消父 context
在这个例子中,childCtx
会因为超时先被取消,而 parentCtx
只有在调用 parentCancel()
后才会取消。
3.5 context.Value() 传递全局数据
虽然 context
的设计主要是用于控制 goroutine 的生命周期,但它也可以通过 context.WithValue
传递少量的全局数据(键值对)。常见的使用场景包括在请求链路中传递用户认证信息、请求 ID 等。
注意:
context.Value()
传递的数据应该是与请求相关的少量数据,而不应当被滥用为数据存储机制。- 使用自定义的键类型(如
type key string
)可以避免键冲突。
type key string
ctx := context.WithValue(context.Background(), key("userID"), "12345")
userID := ctx.Value(key("userID"))
fmt.Println("User ID:", userID)
3.6 context 线程安全
context
是设计为并发安全的。你可以将同一个 context
实例传递给多个 goroutine,而不需要担心数据竞争或同步问题。因为 context
的状态(如取消、超时)通过不可变的方式和通道传播,它天然支持并发。
func worker(ctx context.Context, id int) {select {case <-time.After(3 * time.Second):fmt.Printf("Worker %d done\n", id)case <-ctx.Done():fmt.Printf("Worker %d cancelled\n", id)}
}func main() {ctx, cancel := context.WithCancel(context.Background())for i := 1; i <= 3; i++ {go worker(ctx, i)}time.Sleep(1 * time.Second)cancel() // 取消所有子 goroutinetime.Sleep(4 * time.Second)
}
3.7 层次化的取消信号传播
在 context
结构中,当父 context
被取消时,所有子 context
会自动取消,这是一种层次化的取消信号传播机制。这可以帮助你在复杂系统中控制多个 goroutine 的生命周期。例如,在 Web 服务中,当用户取消请求时,所有与该请求相关的操作都应该立即停止。
在 context
被取消时,context.Err()
会返回具体的错误类型:
context.Canceled
:表示上下文被显式取消(例如,调用了cancel()
)。context.DeadlineExceeded
:表示上下文超时或截止时间已过。
4 底层实现原理
主要通过解析context的源码来剖析第三节中各个特点的实现机理。
4.1 Context接口
type Context interface {Deadline() (deadline time.Time, ok bool)Done() <-chan struct{}Err() errorValue(key any) any
}
这是context最核心的一个接口,定义了Context接口,包含了四个方法,其他结构体只要实现了这四个方法就实现了该接口。四个方法的功能分别是:
- Deadline() (deadline time.Time, ok bool):
Deadline
方法返回一个时间(time.Time
),表示在这个上下文下执行的任务应该取消的截止时间。通常用在需要在某个时间点前完成的任务中。如果context
没有设置任何截止时间,Deadline()
的返回值中,ok
会是false
。这意味着当前context
没有时间限制,因此任务可以无限期地运行,除非被手动取消。对Deadline()
的连续调用会返回相同的结果。 - Done() <-chan struct{}:
Done()
方法返回一个通道,用于通知 goroutine 某个context
关联的任务应该被取消。Done()
返回一个通道(chan struct{}
),这个通道在context
关联的工作需要被取消时会关闭。你可以通过监听这个通道来决定是否继续执行某个操作。 - Err() error:
Err()
方法用于返回context
的取消原因,尤其在Done()
通道关闭之后,它提供了上下文取消的具体原因。如果Done()
通道还没有关闭(即context
没有被取消或超时),调用Err()
方法将返回nil
。如果Done()
通道已经关闭,Err()
方法会返回一个非空的错误(error
),这个错误解释了为什么context
被取消。(1) 如果context
是由于调用cancel()
函数而被取消的,Err()
将返回一个context.Canceled
错误。这表明操作是手动取消的。 (2) 如果context
是因为超时或截止时间到达而被取消的,Err()
将返回一个context.DeadlineExceeded
错误。这表明操作是因为超时被取消的。 - Value(key any) any:
Value()
方法根据传入的键(key
),返回与该键相关联的值。如果该键没有与任何值相关联,则返回nil
。 键的类型可以是支持相等性判断的任何类型(通常是简单的标识符类型)。为了避免不同包之间的键冲突,应该将键定义为未导出类型(即,首字母小写的类型)。
4.2 emptyCtx类
emptyCtx
是一种特殊的上下文,它永远不会被取消、没有任何关联的值,也没有截止时间。它是 context.Background()
和 context.TODO()
的基础类型。
type emptyCtx struct{}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
}
context.Background()
与 context.TODO()
func Background() Context {return backgroundCtx{}
}func TODO() Context {return todoCtx{}
}type backgroundCtx struct{ emptyCtx }func (backgroundCtx) String() string {return "context.Background"
}type todoCtx struct{ emptyCtx }func (todoCtx) String() string {return "context.TODO"
}
context.Background()
:这是 Go 程序中通常用作根上下文的上下文。它一般用于顶层的 context
,例如在启动 goroutine、处理请求、初始化程序时使用。由于它基于 emptyCtx
,所以它不会被取消、没有值或截止时间。
ctx := context.Background()
context.TODO()
:TODO()
通常用于代码还在编写过程中,开发者不确定应该使用哪种 context
的场景下。它是一个临时的占位符,表示将来会替换为合适的上下文。和 Background()
一样,TODO()
也是基于 emptyCtx
,没有取消、值或截止时间。
ctx := context.TODO()
4.3 cancelCtx类
type cancelCtx struct {Contextmu sync.Mutex // protects following fieldsdone atomic.Value // of chan struct{}, created lazily, closed by first cancel callchildren map[canceler]struct{} // set to nil by the first cancel callerr error // set to non-nil by the first cancel callcause error // set to non-nil by the first cancel call
}// A canceler is a context type that can be canceled directly. The
// implementations are *cancelCtx and *timerCtx.
type canceler interface {cancel(removeFromParent bool, err, cause error)Done() <-chan struct{}
}
cancelCtx
是可以被取消的,当 cancelCtx
被取消时,它会自动取消所有实现了 canceler
接口的子上下文。子上下文需要实现 canceler
接口,canceler
接口通常会有一个 cancel()
方法,用于执行取消操作。一旦父上下文的 cancel()
被调用,父上下文会遍历所有子上下文,调用它们的 cancel()
方法,将取消信号逐层向下传递。
func (c *cancelCtx) Value(key any) any {if key == &cancelCtxKey {return c}return value(c.Context, key)
}func (c *cancelCtx) Done() <-chan struct{} {d := c.done.Load()if d != nil {return d.(chan struct{})}c.mu.Lock()defer c.mu.Unlock()d = c.done.Load()if d == nil {d = make(chan struct{})c.done.Store(d)}return d.(chan struct{})
}func (c *cancelCtx) Err() error {c.mu.Lock()err := c.errc.mu.Unlock()return err
}
- func (c *cancelCtx) Value(key any) any:如果
key
与特定的cancelCtxKey
相等,则返回当前cancelCtx
实例本身;否则,调用嵌套的父上下文的Value
方法查找值。 - func (c *cancelCtx) Done() <-chan struct{}:第一次调用
Done()
:如果done
通道还没有创建,程序会在加锁的情况下懒加载创建这个通道。done
通道被存储在c.done
中,确保后续的Done()
调用直接返回这个通道。后续调用Done()
:后续调用Done()
时,会直接通过c.done.Load()
读取已经创建的done
通道,避免不必要的锁操作,提高了性能。 - func (c *cancelCtx) Err() error:在锁保护下获取err
func withCancel(parent Context) *cancelCtx {if parent == nil {panic("cannot create context from nil parent")}c := &cancelCtx{}c.propagateCancel(parent, c)return c
}func (c *cancelCtx) propagateCancel(parent Context, child canceler) {c.Context = parentdone := parent.Done()if done == nil {return // parent is never canceled}select {case <-done:// parent is already canceledchild.cancel(false, parent.Err(), Cause(parent))returndefault:}if p, ok := parentCancelCtx(parent); ok {// parent is a *cancelCtx, or derives from one.p.mu.Lock()if p.err != nil {// parent has already been canceledchild.cancel(false, p.err, p.cause)} else {if p.children == nil {p.children = make(map[canceler]struct{})}p.children[child] = struct{}{}}p.mu.Unlock()return}if a, ok := parent.(afterFuncer); ok {// parent implements an AfterFunc method.c.mu.Lock()stop := a.AfterFunc(func() {child.cancel(false, parent.Err(), Cause(parent))})c.Context = stopCtx{Context: parent,stop: stop,}c.mu.Unlock()return}goroutines.Add(1)go func() {select {case <-parent.Done():child.cancel(false, parent.Err(), Cause(parent))case <-child.Done():}}()
}// parentCancelCtx(parent) 是一个辅助函数,检查父上下文是否是 cancelCtx 类型,
// 或者是否派生自 cancelCtx。
func parentCancelCtx(parent Context) (*cancelCtx, bool) {done := parent.Done()if done == closedchan || done == nil {return nil, false}p, ok := parent.Value(&cancelCtxKey).(*cancelCtx)if !ok {return nil, false}pdone, _ := p.done.Load().(chan struct{})if pdone != done {return nil, false}return p, true
}
propagateCancel
实现了 cancelCtx
中的 propagateCancel
方法,它的作用是将取消信号从父上下文(parent
)传播到子上下文(child
),并根据不同的情况处理取消逻辑。该方法负责建立父子上下文之间的取消关系,当父上下文被取消时,子上下文也会自动取消。这是 Go 语言 context
机制中取消信号传播的核心部分。
func (c *cancelCtx) cancel(removeFromParent bool, err, cause error) {if err == nil {panic("context: internal error: missing cancel error")}if cause == nil {cause = err}c.mu.Lock()if c.err != nil {c.mu.Unlock()return // already canceled}c.err = errc.cause = caused, _ := c.done.Load().(chan struct{})if d == nil {c.done.Store(closedchan)} else {close(d)}for child := range c.children {// NOTE: acquiring the child's lock while holding parent's lock.child.cancel(false, err, cause)}c.children = nilc.mu.Unlock()if removeFromParent {removeChild(c.Context, c)}
}
cancel()
方法用于取消当前上下文,并传播取消信号给它的子上下文。
func removeChild(parent Context, child canceler) {if s, ok := parent.(stopCtx); ok {s.stop()return}p, ok := parentCancelCtx(parent)if !ok {return}p.mu.Lock()if p.children != nil {delete(p.children, child)}p.mu.Unlock()
}
removeChild
将子上下文从父上下文中移除。
4.4 timerCtx类
type timerCtx struct {cancelCtxtimer *time.Timer deadline time.Time
}
timerCtx
是 context
的一种实现,它用于处理超时(timeout)或截止时间(deadline)。通过嵌入 cancelCtx
,timerCtx
实现了取消上下文的能力,并通过计时器控制上下文的超时行为。
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {return WithDeadline(parent, time.Now().Add(timeout))
}func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {return WithDeadlineCause(parent, d, nil)
}func WithDeadlineCause(parent Context, d time.Time, cause error) (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{deadline: d,}c.cancelCtx.propagateCancel(parent, c)dur := time.Until(d)if dur <= 0 {c.cancel(true, DeadlineExceeded, cause) // deadline has already passedreturn c, func() { c.cancel(false, Canceled, nil) }}c.mu.Lock()defer c.mu.Unlock()if c.err == nil {c.timer = time.AfterFunc(dur, func() {c.cancel(true, DeadlineExceeded, cause)})}return c, func() { c.cancel(true, Canceled, nil) }
}
WithTimeou
和WithDeadline
:用于创建带有超时或截止时间的上下文。超时后自动取消上下文,返回context.DeadlineExceeded
错误。WithDeadlineCause
:核心逻辑处理,创建上下文并设置定时器,定时器到期时自动取消上下文。它还支持设置取消原因(cause
)。- 取消函数
CancelFunc
:无论上下文是否超时,调用CancelFunc
都可以立即取消上下文。
func (c *cancelCtx) cancel(removeFromParent bool, err, cause error) {if err == nil {panic("context: internal error: missing cancel error")}if cause == nil {cause = err}c.mu.Lock()if c.err != nil {c.mu.Unlock()return // already canceled}c.err = errc.cause = caused, _ := c.done.Load().(chan struct{})if d == nil {c.done.Store(closedchan)} else {close(d)}for child := range c.children {// NOTE: acquiring the child's lock while holding parent's lock.child.cancel(false, err, cause)}c.children = nilc.mu.Unlock()if removeFromParent {removeChild(c.Context, c)}
}
它会关闭与取消相关的通道,取消所有子上下文,必要时将当前上下文从父上下文中移除,并且在第一次取消时设置取消原因。
4.5 valueCtx类
type valueCtx struct {Contextkey, val any
}
- valueCtx 同样继承了一个 parent context;
- 一个 valueCtx 中仅有一组 kv 对.
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}
}
- 倘若 parent context 为空,panic;
- 倘若 key 为空 panic;
- 倘若 key 的类型不可比较,panic;
- 包括 parent context 以及 kv对,返回一个新的 valueCtx.
func (c *valueCtx) Value(key any) any {if c.key == key {return c.val}return value(c.Context, key)
}func value(c Context, key any) any {for {switch ctx := c.(type) {case *valueCtx:if key == ctx.key {return ctx.val}c = ctx.Contextcase *cancelCtx:if key == &cancelCtxKey {return c}c = ctx.Contextcase withoutCancelCtx:if key == &cancelCtxKey {// This implements Cause(ctx) == nil// when ctx is created using WithoutCancel.return nil}c = ctx.ccase *timerCtx:if key == &cancelCtxKey {return &ctx.cancelCtx}c = ctx.Contextcase backgroundCtx, todoCtx:return nildefault:return c.Value(key)}}
}
- 假如当前 valueCtx 的 key 等于用户传入的 key,则直接返回其 value;
- 假如不等,则从 parent context 中依次向上寻找.