写点什么

Go 语言 context 包实践

作者:FunTester
  • 2024-07-24
    河北
  • 本文字数:5257 字

    阅读完需:约 17 分钟

引子

Java 语言当中,特别是在 Spring 语境下,通常我们会遇到处理上下文的需求。一般场景中,我们可以利用 java.lang.ThreadLocal 来实现,基于线程维度对变量进行管理。ThreadLocal 线程内存储和访问变量的机制,非常适合在单个请求的生命周期内传递上下文信息。


下面是个简单的请求上下文的例子:


public class RequestContext {    private static final ThreadLocal<RequestContext> threadLocal = ThreadLocal.withInitial(RequestContext::new);
private String userId; private String requestId;
public static RequestContext getCurrent() { return threadLocal.get(); }
public static void setCurrent(RequestContext context) { threadLocal.set(context); }
public static void clear() { threadLocal.remove(); }
// Getters and setters public String getUserId() { return userId; }
public void setUserId(String userId) { this.userId = userId; }
public String getRequestId() { return requestId; }
public void setRequestId(String requestId) { this.requestId = requestId; }}
复制代码


使用的话,可以在拦截器中实现初始化赋值或者清楚数据。PS:请注意线程安全的问题和遵守 java.lang.ThreadLocal 最佳实现,切莫自创用法。


Go 语言中,基于 goroutine 进行上下文管理的就是本文的主角 context 包。

简介

Go 语言的 context 包是在 Go 1.7 版本引入的,用于在不同的 goroutine 之间传递请求范围内的值、取消信号和截止日期。它在处理并发操作时非常有用,可以通过 context 对象来控制和管理 goroutine 的生命周期。


context 包的核心类型是 Context 接口,它包含四个方法:DeadlineDoneErrValueDeadline 方法返回操作的截止时间,Done 方法返回一个通道,当操作应该取消时,该通道会关闭,Err 方法返回取消的错误原因,Value 方法允许存储和检索键值对。


在实际使用中,context 包常用于网络请求、数据库操作和其他需要取消和超时控制的操作。通过在函数间传递 Context 对象,可以实现更灵活和可控的并发操作,避免 goroutine 泄漏和资源浪费。

创建方法

Background

在 Go 语言的 context 包中,context.Background() 用于返回一个空的上下文,它通常作为根上下文使用。这个根上下文在整个程序生命周期内存在,永远不会被取消或超时。context.Background() 常用于初始化传递给其他上下文的顶层上下文,例如在启动服务器或处理请求时使用。


ctx := context.Background()
复制代码

TODO

在 Go 语言的 context 包中,context.TODO() 返回一个空的上下文,它与 context.Background() 相似,但其主要用途是作为占位符。通常在代码尚未确定具体上下文需求时使用 context.TODO(),以便稍后替换为适当的上下文。


ctx := context.TODO()
复制代码

WithCancel

在 Go 语言的 context 包中,context.WithCancel 返回一个可取消的上下文及其取消函数。这个函数用于创建一个新的上下文,当调用返回的取消函数时,该上下文及其所有子上下文都会被取消。


ctx, cancel := context.WithCancel(context.Background())  defer cancel()
复制代码

WithDeadline

在 Go 语言的 context 包中,context.WithDeadline 返回一个上下文,该上下文会在指定的时间点自动取消。这种方式对于需要在特定时间点之前完成操作的场景非常有用。


// 创建一个根上下文  rootCtx := context.Background()  // 设置一个未来的时间点  deadline := time.Now().Add(3 * time.Second)  // 基于根上下文创建一个具有截止时间的子上下文  _, cancel := context.WithDeadline(rootCtx, deadline)  cancel() // 确保在操作完成后取消上下文
复制代码

WithTimeout

在 Go 语言的 context 包中,context.WithTimeout 是一个非常常用的函数,它创建一个带有超时的上下文。与 context.WithDeadline 类似,context.WithTimeout 会在指定的时间段后自动取消上下文。这对于需要在限定时间内完成的任务非常有用。


// 创建一个根上下文  rootCtx := context.Background()  // 基于根上下文创建一个具有 2 秒超时的子上下文  _, cancel := context.WithTimeout(rootCtx, 2*time.Second)  defer cancel() // 确保在操作完成后取消上下文
复制代码

WithValue

在 Go 语言的 context 包中,context.WithValue 用于创建一个新的上下文,该上下文携带了特定的键值对。这个功能允许在上下文中传递请求范围内的特定数据,如用户认证信息、配置选项等。与 context.WithCancel 和 context.WithTimeout 不同,context.WithValue 主要用于存储和传递数据,而不是控制上下文的生命周期。


// 创建根上下文  rootCtx := context.Background()  // 使用 WithValue 创建一个新的上下文,并传递用户信息  ctx := context.WithValue(rootCtx, "user", "FunTester")  // 从上下文中检索用户信息  user, ok := ctx.Value("user").(string)  if ok {      fmt.Println("用户:", user)  }
复制代码

常用方法

Deadline

在 Go 语言中,context 包提供了 Deadline 方法,用于获取上下文的截止时间。这在使用 context.WithDeadline 或 context.WithTimeout 创建的上下文时特别有用。


// 创建一个根上下文  rootCtx := context.Background()    // 设置一个 3 秒后的截止时间  deadline := time.Now().Add(3 * time.Second)  ctx, cancel := context.WithDeadline(rootCtx, deadline)  defer cancel() // 确保在操作完成后取消上下文  // 检索截止时间  d, ok := ctx.Deadline()  if ok {      fmt.Println("截止时间:", d.Format("2006-01-02 15:04:05"))  } else {      fmt.Println("没有设置截止时间")  }
复制代码

Done

在 Go 语言的 context 包中,Done 方法是用于获取上下文的取消信号通道。当上下文被取消时,Done 方法返回的通道会接收到一个信号。这对于处理超时、取消操作和清理工作非常重要。


package main    import (      "context"      "fmt"    "time")    func main() {      // 基于根上下文创建一个具有 2 秒超时的上下文      ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)      defer cancel() // 确保在操作完成后取消上下文      // 启动一个 goroutine 执行一些工作      go func(ctx context.Context) {         for {            select {            case <-time.After(1 * time.Second):               fmt.Println("任务进行中")            case <-ctx.Done():               fmt.Println("任务被取消:", ctx.Err())               return            }         }      }(ctx)      // 等待 3 秒钟,以观察超时是否生效      time.Sleep(3 * time.Second)  }
复制代码

Err

在 Go 语言的 context 包中,Err 方法用于获取上下文取消的错误信息。它返回一个错误值,指示上下文的取消原因。这对于确定任务是否因超时、手动取消或其他原因终止非常有用。


package main    import (      "context"      "fmt"    "time")    func main() {      // 创建一个具有 2 秒超时的子上下文      ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)      defer cancel() // 确保在操作完成后取消上下文        // 启动一个 goroutine 执行一些工作      go func(ctx context.Context) {         for {            select {            case <-time.After(1 * time.Second):               fmt.Println("任务进行中")            case <-ctx.Done():               // 使用 Err 方法获取取消原因               err := ctx.Err()               if err != nil {                  fmt.Println("任务被取消:", err)               }               return            }         }      }(ctx)      // 等待 3 秒钟,以观察超时是否生效      time.Sleep(3 * time.Second)      fmt.Println("程序结束")  }
复制代码

Value

在 Go 语言的 context 包中,Value 方法用于从上下文中检索存储的数据。Value 方法允许你在上下文中存储和检索特定的键值对,这对于在上下文中传递请求范围的数据非常有用。


这个例子在之前 WithValue 中已经用到了,这里不再重复。

并发中的应用

goroutine 的取消

在使用 Go 语言进行并发编程时,context包提供了一种优雅的方式来控制 goroutine 的生命周期。通过context.WithCancel函数,我们可以创建一个新的 context 实例,该实例可以被取消。


  • 当主 goroutine 决定不再需要某个操作继续执行时,可以调用 context 的cancel函数。

  • 所有使用该 context 的 goroutine 都可以通过监听 context 的Done()通道来感知到取消信号,并做出相应的清理工作,然后退出。


例如,以下代码展示了如何使用context来控制两个 goroutine 的取消:


package main    import (      "context"      "fmt"    "time")    func main() {      ctx, cancel := context.WithCancel(context.Background())      go func() {         for {            select {            case <-ctx.Done():               // 处理取消逻辑               fmt.Println("goroutine exit, cancel done")               return            default:               // 执行常规任务            }         }      }()      // 主goroutine可以在任何时候调用cancel来停止上述goroutine      time.Sleep(1 * time.Second)      cancel()  }
复制代码

4.2 超时控制

context包同样支持超时控制,这在很多场景下非常有用,比如 API 调用、数据库访问等操作。通过context.WithTimeout函数,我们可以为 context 设置一个超时时间。


  • 当设置的超时时间到达后,context 会自动被取消,所有监听Done()通道的 goroutine 都会收到通知。

  • 这种方式可以防止程序因为某个长时间运行的操作而卡住。


以下示例演示了如何使用context来进行超时控制:


package main    import (      "context"      "fmt"    "time")    func main() {      ctx, cancel := context.WithCancel(context.Background())      go func() {         for {            select {            case <-ctx.Done():               // 处理取消逻辑               fmt.Println("goroutine exit, cancel done")               return            default:               // 执行常规任务            }         }      }()      // 主goroutine可以在任何时候调用cancel来停止上述goroutine      time.Sleep(1 * time.Second)      cancel()  }
复制代码


在 Go 语言中,context包的设计初衷是为了简化并发编程中的一些常见问题,比如在多个goroutine之间传递请求范围的数据、处理超时和取消信号等。它通过提供一个可以被传递给多个函数的请求上下文,使得代码更加清晰和易于管理。

在网络编程中的应用

在 Go 语言中,context包是处理并发请求时不可或缺的工具,尤其是在网络编程中。它允许开发者传递请求范围的值、取消信号和截止时间,从而实现对 HTTP 请求的精细控制。


  • 请求取消:在处理 HTTP 请求时,客户端可能会取消请求。通过context,我们可以检测到这种取消信号,并及时终止正在执行的请求处理逻辑,避免资源浪费。

  • 超时控制:网络请求往往需要设置超时,以避免服务器资源被长时间占用。利用context的超时功能,我们可以为每个请求设置合理的超时时间,提高服务的响应性和健壮性。

  • 请求数据传递:在处理复杂的 HTTP 请求时,我们可能需要在不同的处理阶段传递额外的数据。context提供了一种机制,允许我们将这些数据存储在请求的上下文中,方便跨阶段访问。


以下是context在 HTTP 请求中使用的具体示例:


package main    import (      "context"      "fmt"    "io"        "net/http"        "time"    )    func main() {      client := http.Client{         Timeout: time.Second * 10, // 设置客户端超时时间      }      // 创建一个带有超时的context      ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)      defer cancel()      // 创建一个HTTP请求      req, _ := http.NewRequestWithContext(ctx, http.MethodGet, "http://example.com", nil)      // 发送请求      resp, _ := client.Do(req)      defer resp.Body.Close()      // 读取响应体      body, _ := io.ReadAll(resp.Body)      fmt.Println("Response body:", string(body))  }
复制代码


在这个示例中,我们首先创建了一个http.Client实例,并设置了超时时间。然后,我们使用 context.WithTimeout 创建了一个带有超时的 context,这个 context 被用于创建和发送 HTTP 请求。如果请求在超时时间内没有完成,context会触发取消信号,导致请求被中断。通过这种方式,context包在网络编程中的应用可以显著提高 HTTP 请求处理的灵活性和效率。

发布于: 刚刚阅读数: 5
用户头像

FunTester

关注

公众号:FunTester,800篇原创,欢迎关注 2020-10-20 加入

Fun·BUG挖掘机·性能征服者·头顶锅盖·Tester

评论

发布
暂无评论
Go 语言 context 包实践_FunTester_InfoQ写作社区