文章目录
-
-
- [1. 简介](#1. 简介)
- [2. 常见用法](#2. 常见用法)
-
- [2.1 控制goroutine的生命周期(cancel)](#2.1 控制goroutine的生命周期(cancel))
- [2.2 传递超时(Timeout)信息](#2.2 传递超时(Timeout)信息)
- [2.3 传递截止时间(Deadline)](#2.3 传递截止时间(Deadline))
- [2.4 传递请求范围内的全局数据 (value)](#2.4 传递请求范围内的全局数据 (value))
- [3 特点](#3 特点)
-
- [3.1 上下文的不可变性](#3.1 上下文的不可变性)
- [3.2 链式传递](#3.2 链式传递)
- [3.3 超时和截止时间](#3.3 超时和截止时间)
- [3.4 多级取消机制](#3.4 多级取消机制)
- [3.5 context.Value() 传递全局数据](#3.5 context.Value() 传递全局数据)
- [3.6 context 线程安全](#3.6 context 线程安全)
- [3.7 层次化的取消信号传播](#3.7 层次化的取消信号传播)
- [4 底层实现原理](#4 底层实现原理)
-
- [4.1 Context接口](#4.1 Context接口)
- [4.2 emptyCtx类](#4.2 emptyCtx类)
- [4.3 cancelCtx类](#4.3 cancelCtx类)
- [4.4 timerCtx类](#4.4 timerCtx类)
- [4.5 valueCtx类](#4.5 valueCtx类)
-
1. 简介
在 Go 语言中,context
包主要用于在 并发编程 中控制和管理 goroutine 的生命周期。它提供了一种机制,可以通过传递 context.Context
来协调多个 goroutine,特别是在需要取消操作 、超时控制 和传递共享数据时。
2. 常见用法
2.1 控制goroutine的生命周期(cancel)
context
允许父 goroutine 可以通知子 goroutine 停止工作。例如,当你在 HTTP 服务器中处理一个请求时,如果客户端关闭了连接,你可能不再需要继续处理这个请求。通过 context
,你可以在主流程中取消子流程(即 goroutine),避免不必要的资源消耗。
go
ctx, cancel := context.WithCancel(context.Background())
go func() {
select {
case <-ctx.Done():
// 接收到取消信号,停止工作
fmt.Println("Goroutine canceled")
}
}()
// 取消操作
cancel()
2.2 传递超时(Timeout)信息
context
还可以传递一个超时时间,允许操作在指定时间后自动停止。这对于需要限定时间完成的操作(如数据库查询、API 调用)非常有用。
go
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
),这是在某个具体的时间点之后,所有的操作都应该停止。与超时类似,但它使用绝对时间而不是相对时间 。
go
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 等。
go
package main
import (
"context"
"fmt"
)
// 创建一个 key 类型,用于在 context 中存储和检索数据
type key string
const userIDKey key = "userID"
// 传递用户 ID 的函数
func processRequest(ctx context.Context) {
// 从 context 中取出用户 ID
userID := 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 的 context
ctx := context.WithValue(context.Background(), userIDKey, "12345")
// 传递带有用户 ID 的 context
processRequest(ctx)
// 调用时没有用户 ID
processRequest(context.Background())
}
3 特点
3.1 上下文的不可变性
context
是不可变的。一旦创建了一个 context
,你不能修改它。每次你想要添加新的值、超时或取消机制时,都会创建一个新的子 context
。这有助于保持 context
的并发安全,确保多个 goroutine 可以共享同一个 context
而不发生数据竞争。
例如,使用 context.WithValue
添加键值对时,实际上是创建了一个新的 context
,旧的 context
保持不变。
go
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 秒后自动取消。
go
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
context.WithDeadline
:为上下文设置绝对的截止时间。例如,在某个具体的时间点后取消。
go
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
不受影响。
go
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
)可以避免键冲突。
go
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
的状态(如取消、超时)通过不可变的方式和通道传播,它天然支持并发。
go
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() // 取消所有子 goroutine
time.Sleep(4 * time.Second)
}
3.7 层次化的取消信号传播
在 context
结构中,当父 context
被取消时,所有子 context
会自动取消,这是一种层次化的取消信号传播机制。这可以帮助你在复杂系统中控制多个 goroutine 的生命周期。例如,在 Web 服务中,当用户取消请求时,所有与该请求相关的操作都应该立即停止。
在 context
被取消时,context.Err()
会返回具体的错误类型:
context.Canceled
:表示上下文被显式取消(例如,调用了cancel()
)。context.DeadlineExceeded
:表示上下文超时或截止时间已过。
4 底层实现原理
主要通过解析context的源码来剖析第三节中各个特点的实现机理。
4.1 Context接口
go
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(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()
的基础类型。
go
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()
go
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
,所以它不会被取消、没有值或截止时间。
go
ctx := context.Background()
context.TODO()
:TODO()
通常用于代码还在编写过程中,开发者不确定应该使用哪种 context
的场景下。它是一个临时的占位符,表示将来会替换为合适的上下文。和 Background()
一样,TODO()
也是基于 emptyCtx
,没有取消、值或截止时间。
go
ctx := context.TODO()
4.3 cancelCtx类
go
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
cause 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()
方法,将取消信号逐层向下传递。
go
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.err
c.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
go
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 = parent
done := parent.Done()
if done == nil {
return // parent is never canceled
}
select {
case <-done:
// parent is already canceled
child.cancel(false, parent.Err(), Cause(parent))
return
default:
}
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 canceled
child.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
机制中取消信号传播的核心部分。
go
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 = err
c.cause = cause
d, _ := 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 = nil
c.mu.Unlock()
if removeFromParent {
removeChild(c.Context, c)
}
}
cancel()
方法用于取消当前上下文,并传播取消信号给它的子上下文。
go
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类
go
type timerCtx struct {
cancelCtx
timer *time.Timer
deadline time.Time
}
timerCtx
是 context
的一种实现,它用于处理超时(timeout)或截止时间(deadline) 。通过嵌入 cancelCtx
,timerCtx
实现了取消上下文的能力,并通过计时器控制上下文的超时行为。
go
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 passed
return 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
都可以立即取消上下文。
go
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 = err
c.cause = cause
d, _ := 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 = nil
c.mu.Unlock()
if removeFromParent {
removeChild(c.Context, c)
}
}
它会关闭与取消相关的通道,取消所有子上下文,必要时将当前上下文从父上下文中移除,并且在第一次取消时设置取消原因。
4.5 valueCtx类
go
type valueCtx struct {
Context
key, val any
}
- valueCtx 同样继承了一个 parent context;
- 一个 valueCtx 中仅有一组 kv 对.
go
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.
go
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.Context
case *cancelCtx:
if key == &cancelCtxKey {
return c
}
c = ctx.Context
case withoutCancelCtx:
if key == &cancelCtxKey {
// This implements Cause(ctx) == nil
// when ctx is created using WithoutCancel.
return nil
}
c = ctx.c
case *timerCtx:
if key == &cancelCtxKey {
return &ctx.cancelCtx
}
c = ctx.Context
case backgroundCtx, todoCtx:
return nil
default:
return c.Value(key)
}
}
}
- 假如当前 valueCtx 的 key 等于用户传入的 key,则直接返回其 value;
- 假如不等,则从 parent context 中依次向上寻找.