Golang中的上下文-context包的简介及使用

简介

Go语言中的context包定义了一个名为Context的类型,它定义并传递截止日期、取消信号和其他请求范围的值,形成一个链式模型。如果我们查看官方文档,它是这样说的:

context包定义了Context类型,它在API边界和进程之间传递截止日期、取消信号和其他请求范围的值。

包中的主要实体是Context本身,它是一个接口。它只有四个方法:

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

这里,

  • Deadline:返回应该取消上下文的时间,以及一个布尔值,当没有截止日期时为false
  • Done:返回一个只接收的空结构体通道,它发出上下文应该被取消的信号
  • Err:在完成通道打开时返回nil;否则它返回上下文取消的原因
  • Value:返回与当前上下文的某个键关联的值,如果没有该键的值,则返回nil

与标准库的其他接口相比,Context有许多方法,这些接口通常只有一两个方法。其中三个密切相关:

  • Deadline是取消的时间
  • Done信号上下文完成时
  • Err返回取消的原因

最后一个方法,Value,返回与某个键关联的值。包的其余部分是一系列函数,允许你创建不同类型的上下文。

为什么使用Context

  • 它简化了跨进程或API的截止日期和取消的实现。
  • 它为你的代码扩展做好准备,例如,使用Context会使你的代码清晰易操纵,通过将所有进程以子父关系链式连接,你可以将任何进程绑定/连接在一起。
  • 它易于使用。
  • 它是Goroutine安全的,即你可以在不同的Goroutine上运行相同的上下文而不会泄露。

context.Background()

Background是一个空的上下文,它不会被取消,没有截止日期,也不持有任何值。它主要由主函数用作根上下文或用于测试目的:

package main

import (
	"context"
	"fmt"
)

func main() {
	ctx := context.Background()
	fmt.Println(ctx)
}

输出:

$ go run main.go
context.Background

输出是“context.Background”,它告诉我们这是一个空的Context,它是一个接口,在这个Context接口上,

所有这些数据目前都是nil或空的,因为我们有一个空的Context,即背景上下文,它永远不会被取消,没有截止日期,也没有值。Background通常用于main、init和测试中,以及作为传入请求的顶级上下文。

让我们对所有这些进行fmt.Println检查:

package main

import (
	"context"
	"fmt"
)

func main() {
	ctx := context.Background()
	fmt.Println("ctx.Err() : ", ctx.Err())
	fmt.Println("ctx.Done() : ", ctx.Done())
	fmt.Println("ctx.Value(\"key\") : ", ctx.Value("key"))
	fmt.Print("ctx.Deadline() : ")
	fmt.Print(ctx.Deadline())
}

输出:

$ go run main.go
ctx.Err() :  <nil>
ctx.Done() :  <nil>
ctx.Value("key") :  <nil>
ctx.Deadline() : 0001-01-01 00:00:00 +0000 UTC false

上下文取消函数

Go语言的Context包被用来处理我们的进程或API中的请求流,通过将子上下文与父上下文链接起来,我们可以使用context.WithDeadlinecontext.WithTimeout方法在链中控制截止日期和取消信号。

Context的底层是无法改变的,他在main函数创建,之后传递给其他子函数,比如goroutine,子函数无法改变context,也无法被子进程取消

实例

与done channel不同的是,done是以关闭让其他关闭,而context中的cancel函数则是被调用的

父函数可以取消子函数

如果子函数创建了自己的子函数,也可以把这个context传递下去

那如果子函数想取消自己的子函数呢?

我们可以创建新的context,基于旧的context

我们来举个例子:

func main() {
// 初始化context
	var wg sync.WaitGroup
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

// 添加数据
	generator := func(data string, stream chan any) {
		for {
			select {
			case <-ctx.Done():
				return
			case stream <- data:
			}
		}
	}
	infiniteApples := make(chan any)
	go generator("apple", infiniteApples)

	infiniteBananas := make(chan any)
	go generator("banana", infiniteBananas)

	infiniteOranges := make(chan any)
	go generator("orange", infiniteOranges)

	wg.Add(1)
	go func1(ctx, &wg, infiniteApples)
	func2 := genericFunc
	func3 := genericFunc
	wg.Add(1)
	go func2(ctx, &wg, infiniteBananas)
	wg.Add(1)
	go func3(ctx, &wg, infiniteOranges)

	wg.Wait()
}

func func1(ctx context.Context, s *sync.WaitGroup, streams <-chan any) {
	defer s.Done()
	var wg sync.WaitGroup

	doWOrks := func(CTX context.Context) {
		defer wg.Done()
		for {
			select {
			case <-CTX.Done():
				return
			case d, ok := <-streams:
				if !ok {
					fmt.Println("stream closed")
					return
				}
				fmt.Println(d)
			}
		}
	}
	// 基于父上下文设置自己的上下文
	newCtx, cancel := context.WithTimeout(ctx, 3*time.Second)
	defer cancel()

// 启动自己的子函数
	for i := 0; i < 3; i++ {
		wg.Add(1)
		go doWOrks(newCtx)
	}
	wg.Wait()
}

// 沿用父函数的context
func genericFunc(ctx context.Context, s *sync.WaitGroup, apples chan any) {
	defer s.Done()
	for {
		select {
		case <-ctx.Done():
			return
		case d, ok := <-apples:
			if !ok {
				fmt.Println("stream closed")
				return
			}
			fmt.Println(d)
		}
	}
}

在输出过程中,我们会发现apple先停止输出,最后5秒到了,另外两个goroutine也停止运行 。

上下文值传递

在Go语言中,context上下文可以通过WithValue函数来传递值。这个函数接受一个父上下文(parent Context)、一个键(key)和一个值(value),返回一个新的上下文(Context),这个新上下文与父上下文相同,但是增加了一个键值对。这个键值对可以在上下文的整个传递链中被检索。

以下是一个使用WithValue传递值的例子:

package main

import (
	"context"
	"fmt"
)

func main() {
	// 创建一个带有值的上下文
	ctx := context.WithValue(context.Background(), "language", "Go")

	// 传递ctx到函数中
	process(ctx)
}

func process(ctx context.Context) {
	// 从ctx中检索值
	if language, ok := ctx.Value("language").(string); ok {
		fmt.Println("Language:", language)
	}
}

在这个例子中,我们创建了一个带有键"language"和值"Go"的上下文。然后,我们将这个上下文传递给了process函数,在这个函数中,我们检索并打印出了这个值。

需要注意的是,context的值应该是请求范围的数据,而不是全局的。它通常用于传递请求相关的元数据,如请求ID、用户身份信息等。此外,由于context是并发安全的,所以它可以在多个goroutine之间安全地传递和使用。

建议

  • 每当你需要使用context.Context时,确保它总是作为第一个参数。
  • 始终使用“ctx”作为变量名,虽然使用其他变量名也可以正常工作,但遵循大多数人的做法是好的,像这样的事情你不需要与众不同。
  • 确保调用取消函数。
  • 不要在方法中使用结构体来添加上下文,始终将其作为参数添加,即context.Context
  • 不要过度使用context.WithValue

Reference

  1. Golang中context包使用场景和示例详解
  2. Golang Context Complete Tutorial with Examples
  3. The Go Blog: Go Concurrency Patterns: Context
  • 12
    点赞
  • 31
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Golang ,可以通过 `context` 来获取上下文的所有信息。`context` 提供了一个 `Context` 接口,该接口定义了一组方法,用于获取和设置上下文信息。具体来说,可以通过 `context.Background()` 方法创建一个空的上下文,然后使用 `WithCancel`、`WithDeadline`、`WithTimeout` 或 `WithValue` 等方法来创建一个新的上下文,并在其设置相应的信息。 例如,以下代码演示了如何创建一个具有超时时间的上下文,并在其设置一个字符串值: ``` package main import ( "context" "fmt" "time" ) func doSomething(ctx context.Context) { // 获取上下文的字符串值 value := ctx.Value("key").(string) fmt.Println("value:", value) // 等待上下文超时或被取消 select { case <-ctx.Done(): fmt.Println("context done:", ctx.Err()) case <-time.After(5 * time.Second): fmt.Println("done") } } func main() { // 创建一个具有超时时间的上下文,并设置一个字符串值 ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() ctx = context.WithValue(ctx, "key", "value") // 在新的 Goroutine 执行任务 go doSomething(ctx) // 等待一段时间 time.Sleep(10 * time.Second) } ``` 在这个例子,我们首先使用 `context.Background()` 方法创建了一个空的上下文,然后使用 `context.WithTimeout()` 方法创建了一个具有 3 秒超时时间的新上下文。接着,我们使用 `context.WithValue()` 方法在新上下文设置了一个字符串值。最后,我们在新的 Goroutine 执行了一个任务,并等待一段时间,以便让任务有足够的时间执行。在任务,我们通过 `ctx.Value()` 方法获取了上下文的字符串值,并使用 `select` 语句等待上下文超时或被取消。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值