Go 语言 context 包实践
引子
在 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
接口,它包含四个方法:Deadline
、Done
、Err
和 Value
。Deadline
方法返回操作的截止时间,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请求处理的灵活性和效率。