在 Go 语言中,Context(上下文)是一个非常重要的概念,特别是在处理请求时。
允许在请求的整个生命周期内传递数据、控制请求的取消、处理超时等。
本文将介绍 Go 语言中 Context 的使用,帮助更好地理解与处理请求的传递与控制。
主要内容包括
Context 基础
Context 创建与传递
Context 的超时与取消
Context 的链式操作
Context 在并发中的应用
Context 的应用场景
最佳实践与注意事项
在 Go 语言中,context.Context 接口定义了一个请求的上下文。
它包含了请求的截止时间、取消信号和请求的数据。
使用 Context 可以在请求之间有效地传递数据,同时也可以控制请求的生命周期。
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key interface{}) interface{}
}
Context 接口包含了四个方法:Deadline() 返回 Context 的截止时间
Done() 返回一个通道,它会在 Context 被取消或超时时关闭
Err() 返回 Context 的错误信息,Value(key) 返回 Context 中与 key 关联的值。
package mAIn
import (
"context"
"fmt"
"time"
)
func main() {
// 创建一个根Context
rootContext := context.Background()
// 创建一个带有超时时间的Context,这里设置超时时间为2秒
ctx, cancel := context.WithTimeout(rootContext, 2*time.Second)
defer cancel()
// 在新的goroutine中执行任务
go func(ctx context.Context) {
select {
case <-time.After(3 * time.Second):
fmt.Println("任务完成")
case <-ctx.Done():
fmt.Println("任务取消或超时")
}
}(ctx)
// 等待一段时间,模拟程序运行
time.Sleep(5 * time.Second)
}
在这个例子中,创建了一个带有 2 秒超时时间的 Context,并在一个新的 goroutine 中执行一个任务。
在主 goroutine 中,等待了 5 秒,因此任务在超时之前完成,所以会输出"任务完成"。
package main
import (
"context"
"fmt"
)
type key string
func main() {
// 创建一个根Context
rootContext := context.Background()
// 使用WithValue传递数据
ctx := context.WithValue(rootContext, key("userID"), 123)
// 在子函数中获取传递的数据
getUserID(ctx)
}
func getUserID(ctx context.Context) {
// 从Context中获取数据
if userID, ok := ctx.Value(key("userID")).(int); ok {
fmt.Println("UserID:", userID)
} else {
fmt.Println("UserID不存在")
}
}
在这个示例中,使用 WithValue 方法在 Context 中传递了一个 userID 的值,并在 getUserID 函数中成功获取并打印了这个值。
package main
import (
"context"
"fmt"
"time"
)
func main() {
// 创建一个根Context
rootContext := context.Background()
// 创建一个超时时间为2秒的Context
timeoutCtx, _ := context.WithTimeout(rootContext, 2*time.Second)
// 创建一个手动取消的Context
cancelCtx, cancel := context.WithCancel(rootContext)
defer cancel()
// 在新的goroutine中执行任务
go func(ctx context.Context) {
select {
case <-time.After(3 * time.Second):
fmt.Println("任务完成")
case <-ctx.Done():
fmt.Println("任务取消或超时")
}
}(timeoutCtx)
// 在另一个goroutine中执行任务
go func(ctx context.Context) {
select {
case <-time.After(1 * time.Second):
fmt.Println("另一个任务完成")
case <-ctx.Done():
fmt.Println("另一个任务取消")
}
}(cancelCtx)
// 等待一段时间,模拟程序运行
time.Sleep(5 * time.Second)
}
在上面例子中,用 WithTimeout 方法创建了一个带有 2 秒超时时间的 Context。
在任务的 goroutine 中,用 select 语句监听了超时和 Context 的取消两个事件,以便及时响应。
package main
import (
"context"
"fmt"
"time"
)
func main() {
// 创建一个根Context
rootContext := context.Background()
// 创建一个可以手动取消的Context
ctx, cancel := context.WithCancel(rootContext)
defer cancel()
// 在新的goroutine中执行任务
go func(ctx context.Context) {
select {
case <-time.After(3 * time.Second):
fmt.Println("任务完成")
case <-ctx.Done():
fmt.Println("任务取消")
}
}(ctx)
// 等待一段时间,手动取消任务
time.Sleep(2 * time.Second)
cancel()
// 等待一段时间,模拟程序运行
time.Sleep(1 * time.Second)
}
在上面例子中,使用 WithCancel 方法创建了一个可以手动取消的 Context。
在主函数中,等待了 2 秒后,手动调用 cancel 函数取消了任务。
这时,在任务的 goroutine 中,ctx.Done() 会接收到取消信号,从而退出任务。
在实际应用中,可能需要将多个 Context 串联起来使用。
Go 语言的 Context 提供了 WithCancel、WithDeadline、WithTimeout 等方法。
可以用这些方法实现多个 Context 的协同工作。
package main
import (
"context"
"fmt"
"time"
)
func main() {
// 创建一个根Context
rootContext := context.Background()
// 创建一个超时时间为2秒的Context
timeoutCtx, _ := context.WithTimeout(rootContext, 2*time.Second)
// 创建一个手动取消的Context
cancelCtx, cancel := context.WithCancel(rootContext)
defer cancel()
// 在新的goroutine中执行任务
go func(ctx context.Context) {
select {
case <-time.After(3 * time.Second):
fmt.Println("任务完成")
case <-ctx.Done():
fmt.Println("任务取消或超时")
}
}(timeoutCtx)
// 在另一个goroutine中执行任务
go func(ctx context.Context) {
select {
case <-time.After(1 * time.Second):
fmt.Println("另一个任务完成")
case <-ctx.Done():
fmt.Println("另一个任务取消")
}
}(cancelCtx)
// 等待一段时间,模拟程序运行
time.Sleep(5 * time.Second)
}
在示例中,创建了一个带有 2 秒超时时间的 Context 和一个可以手动取消的 Context,然后分别传递给两个不同的任务。
在主函数中,等待了 5 秒,超时时间为 2 秒,因此第一个任务会因超时而取消,第二个任务则会在 1 秒后完成。
package main
import (
"context"
"fmt"
"sync"
"time"
)
func main() {
// 创建一个根Context
rootContext := context.Background()
// 创建一个可以手动取消的Context
ctx, cancel := context.WithCancel(rootContext)
defer cancel()
// 使用WaitGroup等待所有任务完成
var wg sync.WaitGroup
// 启动多个协程执行任务
for i := 0; i < 5; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
select {
case <-time.After(time.Duration(id) * time.Second):
fmt.Println("任务", id, "完成")
case <-ctx.Done():
fmt.Println("任务", id, "取消")
}
}(i)
}
// 等待一段时间,然后手动取消任务
time.Sleep(2 * time.Second)
cancel()
// 等待所有任务完成
wg.Wait()
}
在上面例子中,创建了一个可以手动取消的 Context,并使用 sync.WaitGroup 等待所有任务完成。
在 for 循环中,启动了 5 个协程,每个协程会等待一段时间后输出任务完成信息。
在主函数中,程序等待了 2 秒后,手动调用 cancel 函数取消了任务,协程会接收到取消信号并退出。
在使用 Context 时,要避免将 Context 放在结构体中。
因为 Context 应该作为函数参数传递,而不应该被放在结构体中进行传递。
Context 应该限定在程序的最小作用域,不要传递到不需要它的函数中。
package main
import (
"fmt"
".NET/http"
"time"
)
func handler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
select {
case <-time.After(2 * time.Second):
fmt.Fprintln(w, "Hello, World!")
case <-ctx.Done():
err := ctx.Err()
fmt.Println("Server:", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
func main() {
http.HandleFunc("/", handler)
http.ListenAndServe(":8080", nil)
}
在上面示例中,创建了一个 HTTP 请求处理函数 handler。
在处理函数中,用 r.Context() 获取到请求的 Context,并在其中执行一个耗时的任务。
如果请求超时,ctx.Done() 会接收到取消信号,可以在其中处理请求超时的逻辑。
package main
import (
"context"
"database/sql"
"fmt"
"time"
_ "Github.com/go-sql-driver/MySQL"
)
func main() {
// 连接数据库
db, err := sql.Open("mysql", "username:password@tcp(localhost:3306)/database")
if err != nil {
fmt.Println("数据库连接失败:", err)
return
}
defer db.Close()
// 创建一个Context,设置超时时间为5秒
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// 在Context的超时时间内执行数据库查询
rows, err := db.QueryContext(ctx, "SELECT * FROM users")
if err != nil {
fmt.Println("数据库查询失败:", err)
return
}
defer rows.Close()
// 处理查询结果
for rows.Next() {
// 处理每一行数据
}
}
在上面例子中,使用 database/sql 包进行数据库查询。创建了一个带有 5 秒超时时间的 Context,并在其中执行数据库查询。
如果查询时间超过 5 秒,Context 会接收到取消信号,可以在其中执行处理查询超时的逻辑。
在其他业务场景中,可使用 Context 实现更多复杂的任务协同。
例如,使用 Context 在多个微服务之间进行数据传递和超时控制。
以下是一个示例,演示了如何在微服务架构中使用 Context 进行跨服务的数据传递
package main
import (
"context"
"fmt"
"time"
)
type Request struct {
ID int
}
type Response struct {
Message string
}
func microservice(ctx context.Context, reqCh chan Request, resCh chan Response) {
for {
select {
case <-ctx.Done():
fmt.Println("Microservice shutting down...")
return
case req := <-reqCh:
// 模拟处理请求的耗时操作
time.Sleep(2 * time.Second)
response := Response{Message: fmt.Sprintf("Processed request with ID %d", req.ID)}
resCh <- response
}
}
}
func main() {
// 创建根Context
rootContext := context.Background()
// 创建用于请求和响应的通道
reqCh := make(chan Request)
resCh := make(chan Response)
// 启动微服务
go microservice(rootContext, reqCh, resCh)
// 创建带有5秒超时时间的Context
ctx, cancel := context.WithTimeout(rootContext, 5*time.Second)
defer cancel()
// 发送请求到微服务
for i := 1; i <= 3; i++ {
req := Request{ID: i}
reqCh <- req
select {
case <-ctx.Done():
fmt.Println("Request timed out!")
return
case res := <-resCh:
fmt.Println(res.Message)
}
}
}
在上面示例中,创建了一个简单的微服务模拟,它接收来自 reqCh 通道的请求,并将处理结果发送到 resCh 通道。
在主函数中,用带有 5 秒超时时间的 Context 来确保请求不会无限期等待,同时也能够处理超时的情况。
通常情况下,应该在函数的参数列表中显式传递 Context,而不是将 Context 放在结构体中。
这样做可以使函数的行为更加明确,避免隐藏传递的 Context,提高代码的可读性和可维护性。
尽管可以将 Context 作为结构体的成员嵌入,但这样的做法通常是不推荐的。
因为 Context 应该是在函数调用的时候传递,而不是嵌入在结构体中。
如果结构体的方法需要使用 Context,应该将 Context 作为参数传递给这些方法。
在实际应用中,要仔细考虑 Context 的传递路径。
若是在多个函数之间传递 Context,确保 Context 的传递路径清晰明了,避免出现歧义和混乱。
Context 的传递路径应该尽量短,不要跨越过多的函数调用。
在 Go 语言中,Context 是一个强大的工具,用于处理请求的传递、控制和超时等。
通过合理地使用 Context,可以编写出更加稳定、高效的异步程序,提高系统的健壮性和可维护性。