上下文-Context

19 篇文章 0 订阅
1 篇文章 0 订阅

Context的作用和意义

Golang这个语言最大的一个优势就是拥有一个高并发利器:goroutine,它是有Golang语言实现的协程,有了它就可以实现高并发请求,但有了大量的协程后,就会带来一些问题,比如:

  • 一些普通参数(例如 LogID、用户 session 等)如何传入协程?
  • 如何可以跟踪goroutine,控制以及终止取消一个协程?
  • 如何做到一个上层任务取消后,所有的下层任务都会被取消?
  • 如何做到中间某一层的任务取消后,只会将当前任务的下层任务取消,而不会影响上层的任务以及同级任务?

Golang中,我们无法从外部终止一个协程,只能它自己结束。常见的比如超时取消等需求,我们通常使用抢占操作或者中断后续操作。

Context 出来以前,Golangchannel + select 的方式来做这件事情的。具体的做法是:定义一个channel,子协程启一个定时任务循环监听这个channel,主协程如果想取消子协程,就往channel里写入信号。

这时我们需要一种优雅的方案来实现这样一种机制,Context就派上用场了。

Context 包的主要作用如下:

  1. 传递请求特定值:在一次请求中,Context可以用于传递请求的特定值,如请求ID、用户信息等等。这些信息可以在整个请求链路中共享,而无需在每个函数调用中显式地传递它们作为参数。
  2. 控制goroutine的取消:Context提供了一种机制来取消一组相关的goroutine,即当父Context被取消时,其所有子Context都会自动取消。这可以避免因长时间的阻塞或等待而导致的资源浪费和应用程序挂起的风险。
  3. 管理请求的截止时间:Context可以用于在请求超时时取消一组相关的goroutine。这可以避免应用程序因等待I/O操作或外部服务响应而被阻塞或挂起。

总之,ContextGo语言中一种非常有用的机制,可以帮助我们在分布式和高并发环境中更轻松地管理上下文信息,以及有效地控制资源的使用和应用程序的响应时间。

Context的实现

Context接口

context.Context是一个接口,接口定义了四个实现的方法:

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

Context接口包含四个方法:

  • Deadline 返回当前上下文的截止时间(deadline)和一个布尔值,表示是否设置了截止时间。如果截止时间已经过期,则返回过期时间和 true

  • Done 返回一个只读的 channel,该 channel 会在当前上下文被取消或超时时关闭;多次调用 Done 方法会返回同一个 Channel

    • 当绑定当前context的任务被取消时,将返回一个关闭的channel(即 closedchan);

    • 如果当前context不会被取消,将返回nil

    • 其他正常情况,返回一个正常的channel

  • Err 返回当前上下文中的错误信息;

    • 如果Done返回的channel没有关闭,将返回nil;
    • 如果Done返回的channel已经关闭,将返回非空的值表示任务结束的原因
    • 如果是context被取消,Err将返回Canceled
    • 如果是context超时,Err将返回DeadlineExceeded
  • Value 返回与当前上下文关联的键(key)的值。键是一个任意类型的值,可以用来查找与之相关联的值。如果在当前上下文中找不到指定键的值,则返回 nil

默认的Context实现

Context 包中实现了4种默认的 Context实现,基本能满足绝大多数的应用场景。下面简单介绍一下:

emptyCtx

emptyCtx是一个int类型的变量,它是 Context 接口的一个实现。emptyCtx 通常用作上下文树的根节点,在没有其他上下文信息可用的情况下,作为默认的上下文。

emptyCtx 的实现非常简单,它不包含任何额外的字段或数据,只是定义了 Context 接口中的方法,并且这些方法都是空的或默认的实现,如下所示:

type emptyCtx int

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 interface{}) interface{} {
	return nil
}

func (e *emptyCtx) String() string {
	switch e {
	case background:
		return "context.Background"
	case todo:
		return "context.TODO"
	}
	return "unknown empty Context"
}

var (
	background = new(emptyCtx)
	todo       = new(emptyCtx)
)

func Background() Context {
	return background
}

func TODO() Context {
	return todo
}

从上述代码中,我们不难发现 emptyCtx通过空方法实现了 context.Context接口中的所有方法,它没有任何功能。

emptyCtx 实例化的两个变量 background todo,分别由两个方法返回:Background() TODO()

一般不会直接使用emptyCtx,而是使用实例化的两个变量,为了正确地处理错误、超时、取消和上下文值等情况,应该尽可能地使用具有明确语义的上下文,例如 context.Background()context.TODO()context.WithTimeout()context.WithCancel()context.WithValue() 等。

以下是一些 emptyCtx 在实际代码中使用的场景:

  1. 在没有传递上下文的情况下,用 emptyCtx 作为默认上下文

    func myFunc() {
        ctx := context.TODO() // 使用 emptyCtx 作为默认上下文
        // ...
    }
    
  2. 在单元测试中,当不需要上下文时使用 emptyCtx

    func TestMyFunc(t *testing.T) {
        ctx := context.TODO() // 在测试中使用 emptyCtx 作为默认上下文
        result, err := myFunc(ctx)
        // ...
    }
    
  3. 在没有传递上下文的情况下,使用 emptyCtx 创建新的上下文

    func myFunc() {
        ctx := context.Background() // 使用 emptyCtx 创建新的上下文
        // ...
    }
    
  4. 在需要处理错误或日志记录的情况下,使用 emptyCtx 作为基础上下文

    func myFunc() {
        ctx := context.Background() // 使用 emptyCtx 创建新的上下文
        // 在上下文中存储错误信息和日志记录
        ctx = context.WithValue(ctx, "error", nil)
        ctx = context.WithValue(ctx, "log", "myFunc called")
        // ...
    }
    

valueCtx

valueCtxContext 接口的一个实现,它通过一个键值对来存储上下文数据。

下面是 valueCtx 的实现代码:

type valueCtx struct {
    Context
    key, val interface{}
}

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

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}
}

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 *timerCtx:
			if key == &cancelCtxKey {
				return ctx.cancelCtx
			}
			c = ctx.Context
		case *emptyCtx:
			return nil
		default:
			return c.Value(key)
		}
	}
}

valueCtx 结构体包含了一个 Context 类型的成员变量,代表其父级上下文,以及一个键值对 keyval,分别表示上下文数据的键和值。

valueCtx 实现了 Context 接口中的 Value() 方法,用于从上下文中获取数据。如果当前上下文中包含了指定的键值对,那么直接返回该值;否则,将调用父级上下文的 Value() 方法来查找该键值对(如果当前context上不存在需要的key,会沿着context链向上寻找key对应的值,直到根节点)。

另外,WithValue() 函数用于创建一个新的 valueCtx 上下文,将给定的键值对存储在上下文中,并将其父级上下文作为参数传递。如果键值对的键为空或不可比较,则会抛出 panic。这里添加键值对不是在原context结构体上直接添加,而是以此context作为父节点,重新创建一个新的valueCtx子节点,将键值对添加在子节点上,由此形成一条context链。

下面是一个示例,演示如何在上下文中存储和使用一个键值对:

func main() {
	emptyCtx := context.Background()
	Context1 := context.WithValue(emptyCtx, "key1", "val1")
	Context2 := context.WithValue(Context1, "key2", "val2")
	Context3 := context.WithValue(Context2, "key3", "val3")
	fmt.Println(Context3.Value("key1"))
}

/* 
---------output------------
val1
*/

在这个示例中,我们可以看出如果当前Context上不存在需要的key, 会沿着context链向上寻找key对应的值,直到根节点。该示例的Context链如图:

在这里插入图片描述

除了以上例子,在处理 HTTP 请求时,可以将请求对象存储在上下文中,以便在后续处理过程中可以轻松地访问请求参数、请求头等信息。示例如下:

func HandleRequest(ctx context.Context, w http.ResponseWriter, r *http.Request) {
    // 将请求对象存储在上下文中
    ctx = context.WithValue(ctx, "request", r)
    // 处理请求
    // ...
}

func ProcessData(ctx context.Context) {
    // 从上下文中获取请求对象
    r, ok := ctx.Value("request").(*http.Request)
    if !ok {
        // 如果请求对象不存在,返回错误
        return errors.New("request not found")
    }
    // 处理数据
    // ...
}

在这个示例中,我们将 HTTP 请求对象存储在上下文中,并通过 Value() 方法在后续处理过程中访问该请求对象。这使得我们可以在整个请求处理过程中方便地访问请求参数、请求头等信息,而不需要在每个函数中都传递请求对象。

cancelCtx

Context 接口的一个实现,它可以通过取消操作来终止整个请求流程。它的作用是允许我们在请求处理过程中取消某个操作,以及通知其它相关操作停止处理,从而达到快速响应和释放资源的目的。

实现如下:

type cancelCtx struct {
	// Context 接口,表示当前上下文的父级上下文
	Context
	//互斥锁,用于保护以下字段的并发修改
	mu sync.Mutex
	//atomic.Value 类型的原子值,用于存储一个 chan struct{} 类型的信道,用于通知取消操作是否完成。该字段会被懒加载,即在第一次调用 WithCancel() 函数创建 cancelCtx 上下文时才会创建并赋值
	done atomic.Value
	//用于存储当前上下文创建的子上下文。子上下文可以通过 WithCancel() 或 WithDeadline() 等函数创建,并会在当前上下文被取消时一并取消。该字段会在第一次调用 cancel() 方法时设置为 nil。
	children map[canceler]struct{}
	//error 类型的变量,用于存储取消操作的错误信息。该字段会在第一次调用 cancel() 方法时设置为 context.Canceled 或 context.DeadlineExceeded。
	err error
	//error 类型的变量,用于存储导致当前上下文被取消的根本原因。该字段会在第一次调用 cancel() 方法时设置为导致当前上下文被取消的根本错误
	cause error
}

type canceler interface {
	cancel(removeFromParent bool, err, cause error)
	Done() <-chan struct{}
}

valueCtx类似,cancelCtx 是更加复杂的一个 ctx,它实现了 canceler 接口,支持取消操作,并且取消操作能够往子节点蔓延。

canceler 接口继承了 Context 接口,并在此基础上增加了以下两个方法:

  • cancel(removeFromParent bool, err, cause error) :用于取消当前上下文及其所有子上下文,并设置取消操作的错误信息和根本原因。该方法会接收三个参数:
    • removeFromParent:一个布尔值,表示当前上下文是否应该从其父级上下文中移除。如果为 true,则表示当前上下文是由其父级上下文创建的子上下文,需要从父级上下文中移除;否则为 false,表示当前上下文是一个独立的上下文。
    • err:一个 error 类型的参数,表示取消操作的错误信息。如果该参数为 nil,则表示取消操作是正常完成的;否则表示取消操作是因为发生了错误而被迫中止的。
    • cause:一个 error 类型的参数,表示导致当前上下文被取消的根本原因。
  • Done() <-chan struct{}:返回一个 chan struct{} 类型的信道,该信道会在当前上下文及其所有子上下文被取消时关闭。

通过实现 canceler 接口,一个上下文类型可以支持取消操作,并且可以设置取消操作的错误信息和根本原因。此外,该接口还提供了一个 Done() 方法,用于在当前上下文及其所有子上下文被取消时通知调用方。该方法返回的信道可以用于阻塞当前协程,直到取消操作完成。

cancelCtx实现的方法:

  • Done() Err() String()方法不多说,比较简单易懂,直接看源码

    //这段代码的作用是返回一个 Done channel,以便在 goroutine 中监听该 channel,以了解是否应该取消该 goroutine。
    func (c *cancelCtx) Done() <-chan struct{} {
    	// 尝试读取已经创建的 Done channel
    	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 {
    		// 如果没有,创建 Done channel
    		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) String() string {
    	return contextName(c.Context) + ".WithCancel"
    }
    
  • Value(key interface{})

    var cancelCtxKey int
    func (c *cancelCtx) Value(key interface{}) interface{} {
    	if key == &cancelCtxKey {
    		return c
    	}
    	return c.Context.Value(key)
    }
    

    可以看到,cancelCtx Value方法提供了一个特殊路径,就是如果传入的 key&cancelCtxKey,那么直接返回当前的 ctx。记住这一点,这点非常有趣。

  • cancel(bool, error)

    取消操作,第一个参数代表取消的时候是否要将当前 ctx 从父 ctx 维护的子 ctx 中移除,第二个参数代表要传给 ctxerr(通过 Context.Err() 方法可以捕获)

    func (c *cancelCtx) cancel(removeFromParent bool, err, cause error) {
    	// 如果没有传入 cancel 错误,则 panic 报错
    	if err == nil {
    		panic("context: internal error: missing cancel error")
    	}
    	// 如果没有传入 cause 错误,则默认将 err 作为 cause
    	if cause == nil {
    		cause = err
    	}
    	// 加锁,保护以下字段
    	c.mu.Lock()
    	if c.err != nil {
    		c.mu.Unlock()
    		return // 已经取消了
    	}
    	c.err = err
    	c.cause = cause
    	// 获取 Done channel,如果没有,则将 done 置为 closedchan
    	d, _ := c.done.Load().(chan struct{})
    	if d == nil {
    		c.done.Store(closedchan)
    	} else {
    		close(d)
    	}
    	// 遍历子节点,并递归地调用 cancel 方法
    	for child := range c.children {
    		child.cancel(false, err, cause)
    	}
    	// 将 children 置为 nil,避免内存泄漏
    	c.children = nil
    	c.mu.Unlock()
    
    	// 如果 removeFromParent 为 true,则将当前节点从父节点的子节点列表中移除
    	if removeFromParent {
    		removeChild(c.Context, c)
    	}
    }
    
    

    cancel 的实现很简单,它会先取消自己(err 赋值,同时关闭 channel),然后将它维护的子节点也给取消掉,最后判断(第一个入参)需不需要将自己从父节点中移除,如果需要的话,就执行 removeChild函数(内部就是调用 delete 内置函数)将自己移除。

    如下图所示,当 cancelCtx1 取消之后,它的子节点 cancelCtx2timerCtx1 以及子节点的子节点 timerCtx2 都会被取消。

    image-20220707144037855

那么如何创建一个可取消的 context 呢?

WithCancel 函数来创建一个可取消的context,即cancelCtx类型的context,源代码如下:

func withCancel(parent Context) *cancelCtx {
	if parent == nil {
		panic("cannot create context from nil parent")
	}
	c := newCancelCtx(parent)
	propagateCancel(parent, c)
	return c
}

func newCancelCtx(parent Context) cancelCtx {
	return cancelCtx{Context: parent}
}

  • 函数先判断传入的 parent 参数是否为 nil,如果为 nil,则会抛出一个 panic 异常。

  • 接着,函数调用 newCancelCtx(parent) 创建了一个新的 cancelCtx 对象 cnewCancelCtx() 函数的作用是,以 parent 作为父上下文,创建一个新的 cancelCtx 对象并返回。新的 cancelCtx 对象会包含一个初始为空的 done 通道和一个空的 children 映射。

  • 然后,函数调用 propagateCancel(parent, c) 将新创建的 cancelCtx 对象 c 注册到其父上下文 parent 中。propagateCancel() 函数的作用是,将新创建的 cancelCtx 对象 c 与其父上下文 parent 关联起来。具体而言,propagateCancel() 函数会将新创建的 cancelCtx 对象 c 添加到其父上下文 parentchildren 映射中。

  • 最后,函数返回新创建的 cancelCtx 对象 c

来看下这里面的propagateCancel函数,该作用是将子 cancelCtx 挂载到父 cancelCtx 上面,这样的话,当父 cancelCtx 被取消之后,它下面挂载的所有子 cancelCtx 都可以被取消。这个方法是实现 cancel 传播的前提。

实现代码如下:

func propagateCancel(parent Context, child canceler) {
	// 获取 parent 的 done 通道
	done := parent.Done()
	// 如果 parent 没有 done 通道,表示 parent 永远不会被取消
	if done == nil {
		return // parent is never canceled
	}

	// 如果 parent 已经被取消,就直接取消 child
	select {
	case <-done:
		child.cancel(false, parent.Err(), Cause(parent))
		return
	default:
	}

	// 如果 parent 是一个 cancelCtx,则将 child 添加到 parent 的 children 中
	if p, ok := parentCancelCtx(parent); ok {
		p.mu.Lock()
		if p.err != nil {
			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()
	} else {
		// 如果 parent 不是 cancelCtx,则启动一个 goroutine 监听 parent 和 child 的取消事件
		goroutines.Add(1)
		go func() {
			select {
			// 如果 parent 被取消,就取消 child
			case <-parent.Done():
				child.cancel(false, parent.Err(), Cause(parent))
			// 如果 child 被取消,就不再关心 parent 是否被取消
			case <-child.Done():
			}
			goroutines.Done()
		}()
	}
}

函数的作用是将父 Context 的取消操作传递给子 Context,也就是当父 Context 取消时,所有与之相关的子 Context 也会被取消。流程如下:

  1. 获取父 Contextdone channel(通过 Done 方法)。
  2. 如果父 Context 没有 done channel,说明它永远不会被取消,直接返回。
  3. 如果父 Context 已经被取消,将错误原因 err 和原因 cause 传递给子 Context 并立即取消它。
  4. 如果父 Context 没有被取消,将子 Context 添加到父 Contextchildren 集合中,并监听子 Context 的 done channel 和父 Contextdone channel,以便在任何一个 channel 被关闭时及时取消子 Context。在这个过程中,需要加锁保护 children 集合。
  5. 如果父 Context 是一个根 Context,启动一个新的 goroutine 监听父 Context 和子 Context 的 done channel,以便在任意一个 channel 被关闭时及时取消子 Context。需要注意的是,这种情况下需要使用 sync.WaitGroup 等待所有 goroutine 执行完成。

下面是一个简单的示例,演示如何在一个 HTTP 服务器中使用 cancelCtx

package main

import (
	"context"
	"fmt"
	"log"
	"net/http"
	"time"
)

func main() {
	mux := http.NewServeMux()
	mux.HandleFunc("/", handler)

	srv := &http.Server{
		Addr:         ":8080",
		Handler:      mux,
		ReadTimeout:  5 * time.Second,
		WriteTimeout: 10 * time.Second,
	}

	go func() {
		<-time.After(15 * time.Second)
		srv.Shutdown(context.Background())
	}()

	log.Println("Server is listening on :8080")
	if err := srv.ListenAndServe(); err != http.ErrServerClosed {
		log.Fatalf("Server failed: %v", err)
	}
}

func handler(w http.ResponseWriter, r *http.Request) {
	ctx, cancel := context.WithCancel(r.Context())
	defer cancel()

	go longRunningTask(ctx)

	select {
	case <-ctx.Done():
		fmt.Fprintf(w, "Request canceled\n")
	case <-time.After(10 * time.Second):
		fmt.Fprintf(w, "Request completed\n")
	}
}

func longRunningTask(ctx context.Context) {
	select {
	case <-time.After(20 * time.Second):
		fmt.Println("Task completed")
	case <-ctx.Done():
		fmt.Println("Task canceled")
	}
}

在上面的例子中,我们创建了一个带有 cancelCtx 的上下文,并将它作为 HTTP 请求处理函数的子上下文。当客户端请求超时或取消时,我们通过调用 cancel() 函数取消这个上下文。在 handler 函数中,我们使用了一个 select 语句,等待长时间运行的任务 longRunningTask 完成或被取消,然后返回相应的消息给客户端。

运行这个程序,然后在浏览器中访问 http://localhost:8080,我们可以看到请求完成或被取消的消息。当我们在 15 秒之后关闭服务器时,我们可以看到 longRunningTask 被取消的消息。

timerCtx

timerCtx 是一个实现了定时器的 Context 实现,它继承了 cancelCtx,并在其基础上增加了一个 timer 和一个 deadline 字段,用于在指定时间后取消该 Context。当 timerCtx 被创建时,会自动创建一个定时器 timer,并在指定的时间后自动取消该 Context。如果在指定时间之前,该 Context 被取消,则会立即关闭 timer

因此,timerCtx 主要用于实现带有超时机制的操作,例如网络请求、资源请求等。在这些场景中,需要在一定时间内得到结果,否则就认为该操作失败。

其结构体定义如下:

type timerCtx struct {
    // 维护一个 cancelCtx
	cancelCtx
    // 通过 cancelCtx.mu 加锁保护
	timer *time.Timer
	// 超时时间
	deadline time.Time
}

timerCtx内部继承了 cancelCtx 的相关变量和方法,还修改了 cancel方法和增加了 Deadline 方法:

//获取该 timerCtx 实例的截止时间
func (c *timerCtx) Deadline() (deadline time.Time, ok bool) {
	return c.deadline, true
}


func (c *timerCtx) cancel(removeFromParent bool, err, cause error) {
  //调用了 cancelCtx 的 cancel 方法,将其标记为已取消状态
	c.cancelCtx.cancel(false, err, cause)
  //如果 removeFromParent 为 true,则将当前 timerCtx 从其父上下文中移除
	if removeFromParent {
		removeChild(c.cancelCtx.Context, c)
	}
  //函数获取 timerCtx 中的锁,停止计时器并将其置为 nil,最后释放锁
	c.mu.Lock()
	if c.timer != nil {
		c.timer.Stop()
		c.timer = nil
	}
	c.mu.Unlock()
}

timeCtx 提供了WithDeadlineWithTimeout 两种方法来实现了定时器的 Context

  • WithDeadline(Context, time.Time)

    WithDeadline返回一个基于parent的可取消的context,并且其过期时间deadline不晚于所设置时间d

    func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
    	// 确保 parent 不为 nil
    	if parent == nil {
    		panic("cannot create context from nil parent")
    	}
    
    	// 如果 parent 的 deadline 已经比 d 更早,则直接使用 WithCancel 创建
    	if cur, ok := parent.Deadline(); ok && cur.Before(d) {
    		return WithCancel(parent)
    	}
    
    	// 创建一个新的 timerCtx 结构体实例
    	c := &timerCtx{
    		cancelCtx: newCancelCtx(parent),
    		deadline:  d,
    	}
    
    	// 将取消信号向下传播给子节点
    	propagateCancel(parent, c)
    
    	// 计算当前时间距离 deadline 的时间差
    	dur := time.Until(d)
    
    	// 如果已经超过了 deadline,直接设置 ctx 的错误为 DeadlineExceeded 并返回
    	if dur <= 0 {
    		c.cancel(true, DeadlineExceeded, nil)
    		return c, func() { c.cancel(false, Canceled, nil) }
    	}
    
    	// 在计时器到期时取消 ctx
    	c.mu.Lock()
    	defer c.mu.Unlock()
    	if c.err == nil {
    		c.timer = time.AfterFunc(dur, func() {
    			c.cancel(true, DeadlineExceeded, nil)
    		})
    	}
    	return c, func() { c.cancel(true, Canceled, nil) }
    }
    
    

    该函数会创建一个新的 timerCtx 实例,其内嵌了一个 cancelCtx,表示在 WithDeadline 函数创建的这个新的 Context 被取消时,所有的子 Context 都会被取消。同时也会创建一个计时器,在计时器到期时自动取消 Context。

    • 如果 parent 的 Deadline 已经比 d 更早,那么直接使用 WithCancel 创建一个新的 Context。

    • 如果已经超过了 d,则直接将 Context 的错误设置为 DeadlineExceeded 并返回。

    • 否则,计算当前时间距离 deadline 的时间差 dur,并创建计时器。当计时器到期时,将 Context 取消,并设置错误为 DeadlineExceeded。同时也提供了一个 CancelFunc 函数,用于取消 Context。

  • WithTimeout(Context, time.Duration)

    WithDeadline类似,WithTimeout也是创建一个定时取消的context,只不过WithDeadline是接收一个过期时间点,而WithTimeout接收一个相对当前时间的过期时长timeout:

    func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
    	return WithDeadline(parent, time.Now().Add(timeout))
    }
    

下面是一个示例演示,使用 timerCtx 实现了一个简单的网络请求,如果请求时间超过 3 秒,就自动取消该请求:

package main

import (
	"context"
	"fmt"
	"io/ioutil"
	"net/http"
	"time"
)

func main() {
	ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(3*time.Second))
	defer cancel()

	req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://test.com.cn/1", nil)
	if err != nil {
		fmt.Println("Error creating request:", err)
		return
	}

	client := &http.Client{}
	resp, err := client.Do(req)
	if err != nil {
		fmt.Println("Error sending request:", err)
		return
	}
	defer resp.Body.Close()

	body, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		fmt.Println("Error reading response body:", err)
		return
	}

	fmt.Println("Response body:", string(body))
}

在这个示例中,我们使用 context.WithDeadline 创建了一个带有截止时间的 context.Context 对象,并传递给 http.NewRequestWithContext 方法,使得该请求受到该上下文的约束。如果请求时间超过 3 秒,则 timerCtx 会自动取消该请求。

Context使用建议

在官方博客里,对于使用 context 提出了几点建议:

  • 不要将 Context 塞到结构体里。直接将 Context 类型作为函数的第一参数,而且一般都命名为 ctx
  • 不要向函数传入一个 nilcontext,如果你实在不知道传什么,标准库给你准备好了一个 context:todo
  • 不要把本应该作为函数参数的类型塞到 context 中,context 存储的应该是一些共同的数据。例如:登陆的 session、cookie 等。
  • 同一个 context 可能会被传递到多个 goroutine,别担心,context 是并发安全的。

参考资料:

ChatGPT https://chat.openai.com/

Go 语言设计与实现 https://draveness.me/golang/docs/part3-runtime/ch06-concurrency/golang-context/

Go进阶训练营 https://lailin.xyz/post/go-training-week3-context.html

simanstar https://blog.csdn.net/simanstar/article/details/121313233

李木子啊 https://www.modb.pro/db/129836

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值