Context的作用和意义
Golang
这个语言最大的一个优势就是拥有一个高并发利器:goroutine
,它是有Golang
语言实现的协程,有了它就可以实现高并发请求,但有了大量的协程后,就会带来一些问题,比如:
- 一些普通参数(例如 LogID、用户 session 等)如何传入协程?
- 如何可以跟踪goroutine,控制以及终止取消一个协程?
- 如何做到一个上层任务取消后,所有的下层任务都会被取消?
- 如何做到中间某一层的任务取消后,只会将当前任务的下层任务取消,而不会影响上层的任务以及同级任务?
在Golang
中,我们无法从外部终止一个协程,只能它自己结束。常见的比如超时取消等需求,我们通常使用抢占操作或者中断后续操作。
在 Context
出来以前,Golang
是channel + select
的方式来做这件事情的。具体的做法是:定义一个channel
,子协程启一个定时任务循环监听这个channel
,主协程如果想取消子协程,就往channel
里写入信号。
这时我们需要一种优雅的方案来实现这样一种机制,Context
就派上用场了。
Context
包的主要作用如下:
- 传递请求特定值:在一次请求中,
Context
可以用于传递请求的特定值,如请求ID、用户信息等等。这些信息可以在整个请求链路中共享,而无需在每个函数调用中显式地传递它们作为参数。 - 控制
goroutine
的取消:Context
提供了一种机制来取消一组相关的goroutine
,即当父Context
被取消时,其所有子Context
都会自动取消。这可以避免因长时间的阻塞或等待而导致的资源浪费和应用程序挂起的风险。 - 管理请求的截止时间:
Context
可以用于在请求超时时取消一组相关的goroutine
。这可以避免应用程序因等待I/O
操作或外部服务响应而被阻塞或挂起。
总之,Context
是Go
语言中一种非常有用的机制,可以帮助我们在分布式和高并发环境中更轻松地管理上下文信息,以及有效地控制资源的使用和应用程序的响应时间。
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
在实际代码中使用的场景:
-
在没有传递上下文的情况下,用
emptyCtx
作为默认上下文func myFunc() { ctx := context.TODO() // 使用 emptyCtx 作为默认上下文 // ... }
-
在单元测试中,当不需要上下文时使用
emptyCtx
func TestMyFunc(t *testing.T) { ctx := context.TODO() // 在测试中使用 emptyCtx 作为默认上下文 result, err := myFunc(ctx) // ... }
-
在没有传递上下文的情况下,使用
emptyCtx
创建新的上下文func myFunc() { ctx := context.Background() // 使用 emptyCtx 创建新的上下文 // ... }
-
在需要处理错误或日志记录的情况下,使用
emptyCtx
作为基础上下文func myFunc() { ctx := context.Background() // 使用 emptyCtx 创建新的上下文 // 在上下文中存储错误信息和日志记录 ctx = context.WithValue(ctx, "error", nil) ctx = context.WithValue(ctx, "log", "myFunc called") // ... }
valueCtx
valueCtx
是 Context
接口的一个实现,它通过一个键值对来存储上下文数据。
下面是 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
类型的成员变量,代表其父级上下文,以及一个键值对 key
和 val
,分别表示上下文数据的键和值。
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
中移除,第二个参数代表要传给ctx
的err
(通过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
取消之后,它的子节点cancelCtx2
和timerCtx1
以及子节点的子节点timerCtx2
都会被取消。
那么如何创建一个可取消的 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
对象c
。newCancelCtx()
函数的作用是,以parent
作为父上下文,创建一个新的cancelCtx
对象并返回。新的cancelCtx
对象会包含一个初始为空的done
通道和一个空的children
映射。 -
然后,函数调用
propagateCancel(parent, c)
将新创建的cancelCtx
对象c
注册到其父上下文parent
中。propagateCancel()
函数的作用是,将新创建的cancelCtx
对象c
与其父上下文parent
关联起来。具体而言,propagateCancel()
函数会将新创建的cancelCtx
对象c
添加到其父上下文parent
的children
映射中。 -
最后,函数返回新创建的
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
也会被取消。流程如下:
- 获取父
Context
的done channel
(通过Done
方法)。 - 如果父
Context
没有done channel
,说明它永远不会被取消,直接返回。 - 如果父
Context
已经被取消,将错误原因 err 和原因 cause 传递给子 Context 并立即取消它。 - 如果父
Context
没有被取消,将子Context
添加到父Context
的children
集合中,并监听子Context
的 done channel 和父Context
的done channel
,以便在任何一个channel
被关闭时及时取消子Context
。在这个过程中,需要加锁保护children
集合。 - 如果父
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
提供了WithDeadline
与 WithTimeout
两种方法来实现了定时器的 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
。 - 不要向函数传入一个
nil
的context
,如果你实在不知道传什么,标准库给你准备好了一个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