MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Go context基本数据结构的演变

2023-11-082.3k 阅读

Go context 的基本概念与重要性

在 Go 语言的并发编程中,context(上下文)起着至关重要的作用。它主要用于在多个 goroutine 之间传递截止时间、取消信号以及其他请求范围的值。随着 Go 应用程序规模和复杂性的增长,对 goroutine 生命周期的管理变得越发关键。context 提供了一种标准且简洁的方式来处理这些问题。

例如,假设我们有一个 HTTP 服务器的场景,当一个请求进来时,服务器可能会启动多个 goroutine 来处理这个请求,比如从数据库获取数据、调用外部 API 等。如果客户端在请求处理过程中取消了请求,或者服务器设置了请求处理的超时时间,我们需要有一种机制能够通知到所有相关的 goroutine 停止工作。context 就是为此而生的。

context 早期版本的数据结构探索

在 Go 早期对 context 功能的初步实现中,并没有如今完善的 context 包。开发者们通常会使用自定义的结构体来传递相关的控制信息。

package main

import (
    "fmt"
    "time"
)

// 早期自定义的上下文结构体
type MyContext struct {
    cancel func()
    timeout time.Duration
}

func worker(ctx MyContext) {
    select {
    case <-time.After(ctx.timeout):
        fmt.Println("工作完成,未超时")
    case <-time.After(ctx.timeout * 2):
        fmt.Println("工作超时模拟")
    case <-make(chan struct{}):
        fmt.Println("工作被取消")
    }
}

func main() {
    timeout := 2 * time.Second
    cancel := func() {}
    ctx := MyContext{cancel, timeout}

    go worker(ctx)

    time.Sleep(3 * time.Second)
}

在上述代码中,MyContext 结构体包含了一个取消函数 cancel 和一个超时时间 timeoutworker 函数通过 select 语句监听超时和取消信号。然而,这种方式存在一些局限性。比如,当一个 goroutine 启动多个子 goroutine 时,如何将 MyContext 传递下去并保证所有子 goroutine 都能正确处理取消和超时,代码会变得复杂且难以维护。

context 包诞生后的初始数据结构

随着 Go 对并发编程需求的进一步重视,标准库中引入了 context 包。最初的 context 包主要定义了 Context 接口以及一些基础的实现。

package context

// Context 接口定义
type Context interface {
    Deadline() (deadline time.Time, ok bool)
    Done() <-chan struct{}
    Err() error
    Value(key interface{}) interface{}
}

Context 接口包含了几个重要的方法:

  1. Deadline:返回当前 Context 的截止时间。如果有截止时间,oktrue,同时返回截止时间 deadline
  2. Done:返回一个只读通道,当 Context 被取消或者超时时,这个通道会被关闭。
  3. Err:返回 Context 被取消或者超时的原因。如果 Context 还未被取消或超时,返回 nil
  4. Value:根据传入的 key 获取对应的值,用于在不同 goroutine 之间传递请求范围的数据。

早期的实现中有两个重要的基础 ContextbackgroundContexttodoContext。它们是所有其他 Context 的根节点。

// backgroundContext 是 context 的根节点
type backgroundContext struct{}

func (bc backgroundContext) Deadline() (time.Time, bool) {
    return time.Time{}, false
}

func (bc backgroundContext) Done() <-chan struct{} {
    return nil
}

func (bc backgroundContext) Err() error {
    return nil
}

func (bc backgroundContext) Value(key interface{}) interface{} {
    return nil
}

var background = backgroundContext{}

// todoContext 也是一个基础的 context
type todoContext struct{}

func (tc todoContext) Deadline() (time.Time, bool) {
    return time.Time{}, false
}

func (tc todoContext) Done() <-chan struct{} {
    return nil
}

func (tc todoContext) Err() error {
    return nil
}

func (tc todoContext) Value(key interface{}) interface{} {
    return nil
}

var todo = todoContext{}

backgroundContexttodoContext 的主要作用是作为所有 Context 树的根。通常,我们会使用 context.Background() 或者 context.TODO() 来创建根 Context,然后基于这个根创建带有取消、超时或者值传递功能的子 Context

例如:

package main

import (
    "context"
    "fmt"
    "time"
)

func main() {
    ctx := context.Background()
    ctx, cancel := context.WithTimeout(ctx, 2*time.Second)
    defer cancel()

    go func(ctx context.Context) {
        select {
        case <-ctx.Done():
            fmt.Println("子 goroutine 被取消或超时,原因:", ctx.Err())
        case <-time.After(3 * time.Second):
            fmt.Println("子 goroutine 正常结束")
        }
    }(ctx)

    time.Sleep(3 * time.Second)
}

在上述代码中,我们首先使用 context.Background() 创建了根 Context,然后通过 context.WithTimeout 创建了一个带有超时功能的子 Context。子 goroutine 通过监听 ctx.Done() 通道来判断是否被取消或超时。

带取消功能的 context 数据结构演变

随着实际应用中对取消功能的频繁使用,context 包进一步发展,出现了 cancelCtx 结构体,它实现了 Context 接口并支持取消功能。

// cancelCtx 结构体实现了取消功能
type cancelCtx struct {
    Context
    mu       sync.Mutex
    done     atomic.Value
    children map[canceler]struct{}
    err      error
}

// 实现取消接口
type canceler interface {
    cancel(removeFromParent bool, err error)
    Done() <-chan struct{}
}

func (c *cancelCtx) Done() <-chan struct{} {
    d := c.done.Load()
    if d != nil {
        return d.(chan struct{})
    }
    c.mu.Lock()
    defer c.mu.Unlock()
    if c.done.Load() == nil {
        c.done.Store(make(chan struct{}))
    }
    return c.done.Load().(chan struct{})
}

func (c *cancelCtx) Err() error {
    c.mu.Lock()
    defer c.mu.Unlock()
    return c.err
}

func (c *cancelCtx) cancel(removeFromParent bool, err error) {
    if err == nil {
        panic("context: internal error: missing cancel error")
    }
    c.mu.Lock()
    if c.err != nil {
        c.mu.Unlock()
        return
    }
    c.err = err
    d, _ := c.done.Load().(chan struct{})
    if d == nil {
        c.done.Store(make(chan struct{}))
        d = c.done.Load().(chan struct{})
    }
    close(d)
    for child := range c.children {
        child.cancel(false, err)
    }
    c.children = nil
    c.mu.Unlock()
    if removeFromParent {
        removeChild(c.Context, c)
    }
}

cancelCtx 结构体包含了一个 Context 嵌入字段,这使得它可以复用基础 Context 的功能。mu 是一个互斥锁,用于保护 donechildrenerr 等字段。done 字段是一个 atomic.Value,用于存储一个通道,当 Context 被取消时,这个通道会被关闭。children 字段是一个 map,用于存储子 canceler,当父 Context 被取消时,会递归地取消所有子 Context

例如:

package main

import (
    "context"
    "fmt"
    "sync"
    "time"
)

func main() {
    var wg sync.WaitGroup
    ctx, cancel := context.WithCancel(context.Background())

    for i := 0; i < 3; i++ {
        wg.Add(1)
        go func(id int, ctx context.Context) {
            defer wg.Done()
            select {
            case <-ctx.Done():
                fmt.Printf("goroutine %d 被取消,原因:%v\n", id, ctx.Err())
            case <-time.After(2 * time.Second):
                fmt.Printf("goroutine %d 正常结束\n", id)
            }
        }(i, ctx)
    }

    time.Sleep(1 * time.Second)
    cancel()
    wg.Wait()
}

在这段代码中,我们使用 context.WithCancel 创建了一个可取消的 Context。多个 goroutine 通过监听 ctx.Done() 通道来处理取消信号。当 cancel 函数被调用时,所有相关的 goroutine 都会收到取消信号并进行相应处理。

带超时功能的 context 数据结构演变

除了取消功能,超时功能也是 context 包的重要组成部分。timerCtx 结构体就是为实现超时功能而设计的。

// timerCtx 结构体实现了超时功能
type timerCtx struct {
    cancelCtx
    timer *time.Timer
    deadline time.Time
}

func (c *timerCtx) Deadline() (time.Time, bool) {
    return c.deadline, true
}

func (c *timerCtx) cancel(removeFromParent bool, err error) {
    c.cancelCtx.cancel(false, err)
    if c.timer != nil {
        c.timer.Stop()
    }
    if removeFromParent {
        removeChild(c.cancelCtx.Context, c)
    }
}

timerCtx 结构体嵌入了 cancelCtx,这意味着它不仅拥有取消功能,还增加了超时功能。timer 字段是一个 time.Timer,用于在设定的超时时间到达时触发取消操作。deadline 字段记录了截止时间。

例如:

package main

import (
    "context"
    "fmt"
    "time"
)

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
    defer cancel()

    select {
    case <-ctx.Done():
        fmt.Println("操作超时,原因:", ctx.Err())
    case <-time.After(3 * time.Second):
        fmt.Println("操作正常结束")
    }
}

在这个例子中,我们使用 context.WithTimeout 创建了一个带有 2 秒超时的 Context。通过 select 语句监听 ctx.Done() 通道,当超时时间到达时,ctx.Done() 通道会被关闭,从而执行相应的超时处理逻辑。

可携带值的 context 数据结构演变

为了在不同 goroutine 之间传递请求范围的数据,context 包引入了 valueCtx 结构体。

// valueCtx 结构体用于携带值
type valueCtx struct {
    Context
    key, val interface{}
}

func (c *valueCtx) Value(key interface{}) interface{} {
    if c.key == key {
        return c.val
    }
    return c.Context.Value(key)
}

valueCtx 结构体同样嵌入了 Context 接口,keyval 字段分别用于存储键值对。Value 方法用于根据传入的 key 获取对应的值,如果当前 Context 中的 key 与传入的 key 匹配,则返回对应的值,否则调用嵌入的 ContextValue 方法继续查找。

例如:

package main

import (
    "context"
    "fmt"
)

func main() {
    ctx := context.WithValue(context.Background(), "userID", 12345)

    value := ctx.Value("userID")
    if value != nil {
        fmt.Println("获取到的值:", value)
    }
}

在上述代码中,我们使用 context.WithValue 创建了一个携带 userID 值的 Context,然后通过 ctx.Value 方法获取这个值。

context 数据结构演变带来的影响

  1. 代码简洁性:随着 context 数据结构的不断演变,开发者在处理并发控制和数据传递时,代码变得更加简洁和易读。例如,早期自定义结构体实现上下文控制的复杂逻辑被标准库中简洁的接口和结构体所替代。
  2. 可维护性:标准化的 context 数据结构使得代码的维护更加容易。不同开发者在使用 context 时遵循相同的规范,减少了因自定义实现不一致而导致的问题。
  3. 功能扩展性context 数据结构的演变过程中不断增加新的功能,如超时、取消和值传递等,使得 Go 的并发编程能够更好地适应各种复杂的场景。

总结 context 基本数据结构演变的要点

从早期自定义结构体实现上下文功能,到标准库中 context 包的诞生,再到 cancelCtxtimerCtxvalueCtx 等结构体的出现,Go 的 context 基本数据结构经历了一个逐步完善的过程。这个过程不仅提升了 Go 语言在并发编程方面的能力,也为开发者提供了更加便捷、高效的工具来管理 goroutine 的生命周期和传递请求范围的数据。理解这些数据结构的演变,有助于开发者在编写并发程序时更好地利用 context 的功能,编写出更加健壮、可靠的代码。