浅析Golang的Context

文章目录

      • [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.WithCancelcontext.WithTimeout 时,都会基于父 context 创建新的子 context

3.3 超时和截止时间

context 允许你为操作设置 超时时间截止时间 。通过 context.WithTimeoutcontext.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
}

timerCtxcontext 的一种实现,它用于处理超时(timeout)或截止时间(deadline) 。通过嵌入 cancelCtxtimerCtx 实现了取消上下文的能力,并通过计时器控制上下文的超时行为。

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) }
}
  • WithTimeouWithDeadline:用于创建带有超时或截止时间的上下文。超时后自动取消上下文,返回 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 中依次向上寻找.
相关推荐
包饭厅咸鱼4 分钟前
QML----复制指定下标的ListModel数据
开发语言·数据库
bryant_meng11 分钟前
【python】Distribution
开发语言·python·分布函数·常用分布
红黑色的圣西罗16 分钟前
Lua 怎么解决闭包内存泄漏问题
开发语言·lua
yanlou23317 分钟前
KMP算法,next数组详解(c++)
开发语言·c++·kmp算法
小林熬夜学编程18 分钟前
【Linux系统编程】第四十一弹---线程深度解析:从地址空间到多线程实践
linux·c语言·开发语言·c++·算法
2401_8576226628 分钟前
SpringBoot健身房管理:敏捷与自动化
spring boot·后端·自动化
墨墨祺29 分钟前
嵌入式之C语言(基础篇)
c语言·开发语言
程序员阿龙30 分钟前
基于SpringBoot的医疗陪护系统设计与实现(源码+定制+开发)
java·spring boot·后端·医疗陪护管理平台·患者护理服务平台·医疗信息管理系统·患者陪护服务平台
躺不平的理查德41 分钟前
数据结构-链表【chapter1】【c语言版】
c语言·开发语言·数据结构·链表·visual studio
程思扬1 小时前
为什么Uptime+Kuma本地部署与远程使用是网站监控新选择?
linux·服务器·网络·经验分享·后端·网络协议·1024程序员节