Go 语言 context 包实践

2024-07-30 16:26:02 浏览数 (2)

引子

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

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

代码语言:javascript复制
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() 常用于初始化传递给其他上下文的顶层上下文,例如在启动服务器或处理请求时使用。

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

TODO

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

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

WithCancel

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

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

WithDeadline

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

代码语言:javascript复制
// 创建一个根上下文  
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 会在指定的时间段后自动取消上下文。这对于需要在限定时间内完成的任务非常有用。

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

WithValue

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

代码语言:javascript复制
// 创建根上下文  
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 创建的上下文时特别有用。

代码语言:javascript复制
// 创建一个根上下文  
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 方法返回的通道会接收到一个信号。这对于处理超时、取消操作和清理工作非常重要。

代码语言:javascript复制
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 方法用于获取上下文取消的错误信息。它返回一个错误值,指示上下文的取消原因。这对于确定任务是否因超时、手动取消或其他原因终止非常有用。

代码语言:javascript复制
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的取消:

代码语言:javascript复制
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来进行超时控制:

代码语言:javascript复制
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请求中使用的具体示例:

代码语言:javascript复制
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请求处理的灵活性和效率。

0 人点赞