GO: Context使用

本文详细介绍了Go语言中的Context概念,包括其在并发和超时控制中的作用,以及如何通过WithCancel、WithDeadline、WithTimeout和WithValue方法创建派生上下文。此外,还讨论了Context的最佳实践,如避免使用nil Context,以及在使用WithValue传递参数时的注意事项。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

概念

  • Context即Goroutine的上下文,一般用来处理并发和控制超时,控制goroutine的生命周期,用更少的计算资源同步信号

  • context也可以用于传递上下文信息,例如在网络编程场景下,如果一个request要经手多个goroutine,就可以通过context把这个request的信息传递到不同的goroutine里面

  • context是并发安全的,同一个context可以传递给不同的goroutine

context核心结构和方法

接口方法

context需要实现context.Context接口,该接口定义了四个接口方法,四个接口方法幂等(连续多次调用得到的结果是一致的)。context包内已经实现了该结构,可以通过跟上下文方法进行调用使用。

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

Deadline()
返回 context.Context 被取消的时间,也就是完成工作的截止日期
Done()
返回一个 Channel,这个 Channel 会在当前工作完成或者上下文被取消后关闭,多次调用 Done 方法会返回同一个 Channel
Err()
返回 context.Context 结束的原因,它只会在 Done 方法对应的 Channel 关闭时返回非空的值;

  • 如果 context.Context 被取消,会返回 Canceled 错误;
  • 如果 context.Context 超时,会返回 DeadlineExceeded 错误;

Value()
可以从context预存的键值对中拿取预设值,没有对应的key值时返回nil

根上下文方法

context中的接口context.Context不用我们人为去实现,context包内部已经有实现好的给我们使用,可以通过根上下文context.Background()context.TODO()来获得。context.Background()context.TODO()用于创建根上下文,二者功能基本一样,只是使用场景不太一样:

context.Background() 是上下文的默认值,是根上下文;
context.TODO() 一般在不确定应该使用哪种上下文时使用;

一般情况都是使用context.Background()作为起始上下文。

派生上下文方法

如果需要context附带一些功能(传参、定时)可以在根上下文基础上使用派生上下文WithCancel()WithDeadline()WithTimeout(), WithValue()生成有指定功能的子上下文来使用。
使用WithCancel()WithDeadline()WithTimeout(), WithValue()可以从根上下文中派生出有特定功能(定时,传参)新的上下文。当根上下文被取消时,使用上述方法派生出来的子上下文也会被取消。

func WithCancel(parent Context) (ctx Context, cancel CancelFunc)  // 手动取消协程
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)  // 定时取消协程
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)  // 超时取消协程
func WithValue(parent Context, key, val interface{}) Context  // 传参

WithCancel()

手动取消协程: 使用该方法可以使用cacel函数来手动取消协程。

ctx, cancel := context.WithCancel(context.Background())
defer cancel()

WithDeadline()

定时取消协程: 在设定的时间点之后取消协程。

d := time.Now().Add(50 * time.Millisecond)
ctx, cancel := context.WithDeadline(context.Background(), d)

// Even though ctx will be expired, it is good practice to call its
// cancelation function in any case. Failure to do so may keep the
// context and its parent alive longer than necessary.
defer cancel()

select {
case <-time.After(1 * time.Second):
    fmt.Println("overslept")
case <-ctx.Done():
    fmt.Println(ctx.Err())
}

WithTimeout()

超时取消协程: 在一定时间段后取消协程。和定时取消协程的区别是定时设置的是时间点,超时设置的是时间段。

ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
defer cancel()

select {
case <-time.After(1 * time.Second):
    fmt.Println("overslept")
case <-ctx.Done():
    fmt.Println(ctx.Err()) // prints "context deadline exceeded"
}

WithValue()

通过WithValue()形成的context实际上是在父级context的基础上增加了键值对形成的新context实例,所以原理上一个context只有一个键值对,如果想在context添加多个键值对,需要重复调用WithValue()函数来形成层叠的context结构,如下图所示:
在这里插入图片描述
WithValue()的常见使用场景是传递请求对应用户的认证令牌以及用于进行分布式追踪的请求 ID。
WithValue()使用注意点1:在gRPC中传参
grpc 中不能使用WithValue()在client和server之间传参,需要使用metadata接口替代:

package main

import (
    "context"
    "fmt"
    "google.golang.org/grpc/metadata"
)

func main() {
    // client
    md := metadata.Pairs(
        "k1", "v1",
        "k2", "v2",
    )
    ctx := metadata.NewOutgoingContext(context.Background(), md)

    // server
    md2, ok := metadata.FromIncomingContext(ctx)
    if !ok {
        panic("no metadata")
    }

    fmt.Printf("k1 %v\n", md2["k1"])
    fmt.Printf("k2 %v\n", md2["k2"])
}

其中使用NewOutgoingContext传参,FromIncomingContext取参。

WithValue()使用注意点2:尽量使用自定义类型的key值
这一点在go的官方文档注释里面有描述。

package user

import "context"

// User is the type of value stored in the Contexts.
type User struct {...}

// key is an unexported type for keys defined in this package.
// This prevents collisions with keys defined in other packages.
type key int

// userKey is the key for user.User values in Contexts. It is
// unexported; clients use user.NewContext and user.FromContext
// instead of using this key directly.
var userKey key

// NewContext returns a new Context that carries value u.
func NewContext(ctx context.Context, u *User) context.Context {
	return context.WithValue(ctx, userKey, u)
}

// FromContext returns the User value stored in ctx, if any.
func FromContext(ctx context.Context) (*User, bool) {
	u, ok := ctx.Value(userKey).(*User)
	return u, ok
}

Demo

使用context终止协程

func main() {
	ctx, cancel := context.WithCancel(context.Background())
	go task(ctx,"task1")
	go task(ctx,"task1")
	go task(ctx,"task1")

	time.Sleep(10 * time.Second)
	fmt.Println("time's up, stop task.")
	cancel()
	
	time.Sleep(5 * time.Second)
}

func task(ctx context.Context, name string) {
	for {
		select {
		case <-ctx.Done():
			fmt.Println(name," stopped.")
			return
		default:
			fmt.Println(name," doing.")
			time.Sleep(2 * time.Second)
		}
	}
}

Best Practice

  • 不要传入nil的context到函数中
  • 使用WithValue()函数传参时,尽量自定义key的类型
  • 尽量少用WithValue()传递请求参数,该方法一般使用场景是传递请求对应用户的认证令牌以及用于进行分布式追踪的请求 ID
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值