likes
comments
collection
share

golang中context上下文的使用方法以及实现原理

作者站长头像
站长
· 阅读数 21

序言

本文通过编写使用context的测试案例,并简要介绍了少量接口源码,简单阐述了Golang中context上下文的使用方法、适用场景及其实现机制。

1. 什么是context上下文

什么是上下文,顾名思义,上下文就是上文和下文,能够将上文和下文数据串连起来的一种机制,golang中的上下文就是指API之间或者方法调用之间所传递的业务参数之外的额外信息,比如客户端ip地址、请求接收的时间、客户端身份信息以及函数的超时机制信息等。

2. 使用方法及原理

2.1 基本方法

golang中的context提供两个生成顶层context的方法 1.context.BackBround():返回一个空的context不会被取消的context,一般用在主函数、初始化以及创建根的时候。 2.context.TODO():和context.BackBround()方法一样,一般用在不确定传递什么上下文信息的时候使用,和第一个方法的底层实现其实是一模一样的,名字不同只是为了提高程序的可读性。 Context接口中context使用的一些方法:

type Context interface {
    Deadline() (deadline time.Time, ok bool)// 返回context的截止日期
    Done() <-chan struct{}//返回一个channel对象context被取消时此channel会被关闭
    Err() error//当context被取消时,可调用Err获取报错信息或context被取消的原因
    Value(key any) any//返回此context指定key对应的value
}

在使用context的时候有几个约定俗成的规则: 1.函数创建时,就要把context放在第一个参数的位置 2.不要使用nil充当context类型的参数值,可以使用context.Backgroud创建一个 3.context中key的类型最好不要使用string,最好使用空接口或自定义别名,保证传递过程中,其他人使用到你的上下文时不会发生key的冲突即可。 例如:

// 定义一个空结构体类型作为键的基础类型 
type contextKey struct{} // 定义一个键实例,导出为接口类型
var UserKey interface{} = contextKey{} 
func main() { 
    // 创建一个带有值的上下文
    ctx := context.WithValue(context.Background(), UserKey, "Alice") 
    // 从上下文中检索值
    if user, ok := ctx.Value(UserKey).(string); ok {
        fmt.Println("User:", user)
    } else { 
        fmt.Println("User not found") 
    } 
}

2.2 特殊方法

context标准库中还有一些特殊用来创建context的方法,可以传递父context信息

2.2.1 WithValue()带键值对的上下文

WithValue()方法是基于父context生成一个新的context,而这个新的context底层是一个内嵌了父context的结构体,所以这样就可以使用WithValue方法来实现一条context链,当子context查找某个键没找到还可以去父context中继续查找。 嵌套context和withValue源码

type valueCtx struct {
    Context
    key, val any// 嵌套的key-value
}
// WithValue接收一个父context,和一个键值对,返回一个子context,内部嵌套了父context
func WithValue(parent Context, key, val any) Context {
   //此处panic省略
    return &valueCtx{parent, key, val}
}

测试案例:当子context没有某个key,还可以去父context获取

// 测试WithValue方法,查找某个键,子context没有会继续去父context查找
type contextKey1 struct{}
type contextKey2 struct{}

var UserKey1 = &contextKey1{}
var UserKey2 = &contextKey2{}

func main() {
    ctx := context.TODO()
    ctx = context.WithValue(ctx, UserKey1, "UserKey1")// 父context
    ctx = context.WithValue(ctx, UserKey2, "UserKey2")// 子context
    fmt.Println(ctx.Value(UserKey1))
}

执行结果: golang中context上下文的使用方法以及实现原理

2.2.2 WithCancel()带取消功能的上下文

WithCancel()接收一个上下文对象,返回一个该上下文的副本和一个cancel方法,调用cancel可以取消原来的上下文,这里返回的上下文对象其实也是一个cacelCtx取消上下文类型,和上面的valueCtx类似,它实现了原生的context接口,并且添加了与cancel取消机制有关的结构 cancelCtx取消续上下文源码:

type cancelCtx struct {
	Context//实现原生的context接口
	mu       sync.Mutex            // 保护以下字段
	done     atomic.Value          // chan 类型,延迟创建,在第一次取消调用时关闭
	children map[canceler]struct{} // 管理取消上下文的子接口
	err      error                 // 记录错误信息
	cause    error                 
}

测试案例:创建一个带取消功能的上下文,可以定时主动取消

func main() {
    // 创建一个带有取消功能的上下文和取消函数
    ctx, cancel := context.WithCancel(context.Background())
    // 启动一个并发任务,传递上下文
    go doWork(ctx)
    // 模拟一段时间后取消操作
    time.Sleep(1 * time.Second)
    cancel() // 取消操作
    // 等待任务完成
    time.Sleep(1 * time.Second)
    fmt.Println("Main goroutine completed")
}
func doWork(ctx context.Context) {
    // 模拟一些工作
    for {
       select {
       case <-ctx.Done():
          fmt.Println("Worker goroutine received cancel signal")
          return
       default:
          // 模拟一些工作
          time.Sleep(500 * time.Millisecond)// 半秒打印一次
          fmt.Println("Working...")
       }
    }
}

结果:

golang中context上下文的使用方法以及实现原理

2.2.3 WithCancel 和 WithValue使用注意

带父子关系的上下文链路,当父context被取消了之后,父上下文的取消信号通过内部的 done 通道传播给子上下文,所以子context同样也会立即被取消 测试案例:

func main() {
    // 创建一个根上下文
    rootCtx := context.Background()
    // 使用 WithCancel 创建一个取消上下文和取消函数
    ctx, cancel := context.WithCancel(rootCtx)
    defer cancel()
    // 在根上下文中添加一个值
    ctxWithValue := context.WithValue(ctx, "key1", "value1")
    // 启动一个 goroutine 来处理任务
    go func(ctx context.Context) {
       // 在子上下文中进行工作
       for {
          select {
          case <-ctx.Done()://接收子上下文取消信号
             fmt.Println("Child context received cancel signal")
             return
          default:
             // 获取并打印上下文中的值
             value := ctx.Value("key1")
             fmt.Printf("Working with value: %v\n", value)

             // 模拟一些工作
             time.Sleep(500 * time.Millisecond)
          }
       }
    }(ctxWithValue)
    // 模拟一段时间后取消根上下文
    time.Sleep(1 * time.Second)
    cancel()
    // 等待 goroutine 执行完成
    time.Sleep(500 * time.Millisecond)
    fmt.Println("Main goroutine completed")
}

运行结果:可以看到,当父上下文cancel(),子上下文也会接收到消息,也取消了

golang中context上下文的使用方法以及实现原理

2.2.3 WithTimeout()和WithDeadline()

这两个方法其实十分接近,都是跟上下文时间有关,只不过 WithTimeout()带是超时时间的上下文,而WithDeadline()是带截止时间的上下文,一个是以起点开始计时,一个是只管时间终点不管时间起点。时间过期或到了指定时间,context会自动取消,用法和上面的两个方法接近。

下节预告:golang中channel通道的使用技巧以及实现原理

转载自:https://juejin.cn/post/7391699542007169033
评论
请登录