<返回更多

Go语言高级特性:Context深入解读

2023-11-01  微信公众号  Go先锋
加入收藏

概述

在 Go 语言中,context(上下文)是一个非常重要的概念。

它主要用于在多个 goroutine 之间传递请求特定任务的截止日期、取消信号以及其他请求范围的值。3. Context 的取消与超时

本文将探讨 Go 语言中context的用法,从基础概念到实际应用,将全面了解上下文的使用方法。

主要内容包括

什么是 Context(上下文)

Context 的基本用法:创建与传递

Context 的取消与超时

Context 的值传递

实际应用场景:HTTP 请求的 Context 使用

数据库操作中的 Context 应用

自定义 Context 的实现

Context 的生命周期管理

Context 的注意事项

1. 什么是 Context(上下文)

在 Go 语言中,context(上下文)是一个接口,定义了四个方法:Deadline()、Done()、Err()和Value(key interface{})。

它主要用于在 goroutine 之间传递请求的截止日期、取消信号以及其他请求范围的值。

2. Context 的基本用法:创建与传递

2.1 创建 Context

package mAIn


import (
  "context"
  "fmt"
)


func main() {
  // 创建一个空的Context
  ctx := context.Background()


  // 创建一个带有取消信号的Context
  _, cancel := context.WithCancel(ctx)
  defer cancel() // 延迟调用cancel,确保在函数退出时取消Context


  fmt.Println("Context创建成功")
}

以上代码演示了如何创建一个空的context和一个带有取消信号的context。

使用context.WithCancel(parent)可以创建一个带有取消信号的子context。

在调用cancel函数时,所有基于该context的操作都会收到取消信号。

2.2 传递 Context

package main


import (
  "context"
  "fmt"
  "time"
)


func worker(ctx context.Context) {
  for {
    select {
    case <-ctx.Done():
      fmt.Println("收到取消信号,任务结束")
      return


    default:
      fmt.Println("正在执行任务...")
      time.Sleep(1 * time.Second)
    }


  }
}


func main() {
  ctx := context.Background()


  ctxWithCancel, cancel := context.WithCancel(ctx)


  defer cancel()


  go worker(ctxWithCancel)


  time.Sleep(3 * time.Second)
  cancel() // 发送取消信号
  time.Sleep(1 * time.Second)
}

在上面例子中,创建了一个带有取消信号的context,并将它传递给一个 goroutine 中的任务。

调用cancel函数,可以发送取消信号,中断任务的执行。

3.Context 的取消与超时

3.1 使用 Context 实现取消

package main


import (
  "context"
  "fmt"
  "time"
)


func main() {
  ctx := context.Background()
  ctxWithCancel, cancel := context.WithCancel(ctx)


  go func() {
    select {
    case <-ctxWithCancel.Done():
      fmt.Println("收到取消信号,任务结束")
    case <-time.After(2 * time.Second):
      fmt.Println("任务完成")
    }
  }()


  time.Sleep(1 * time.Second)
  
  cancel() // 发送取消信号
  
  time.Sleep(1 * time.Second)
}

在这个例子中,使用time.After函数模拟一个长时间运行的任务。

如果任务在 2 秒内完成,就打印“任务完成”,否则在接收到取消信号后打印“收到取消信号,任务结束”。

3.2 使用 Context 实现超时控制

package main


import (
  "context"
  "fmt"
  "time"
)


func main() {
  ctx := context.Background()
  
  ctxWithTimeout, cancel := context.WithTimeout(ctx, 2*time.Second)
  
  defer cancel()


  select {
  case <-ctxWithTimeout.Done():
    fmt.Println("超时,任务结束")
  case <-time.After(1 * time.Second):
    fmt.Println("任务完成")
  }
}

在上面例子中,使用context.WithTimeout(parent, duration)函数创建了一个在 2 秒后自动取消的context

如果任务在 1 秒内完成,就打印“任务完成”,否则在 2 秒后打印“超时,任务结束”。

4. Context 的值传递

4.1 使用 WithValue 传递值

package main


import (
  "context"
  "fmt"
)


type key string


func main() {
  ctx := context.WithValue(context.Background(), key("name"), "Alice")
  value := ctx.Value(key("name"))
  fmt.Println("Name:", value)
}

在上面例子中,使用context.WithValue(parent, key, value)函数在context中传递了一个键值对。

使用ctx.Value(key)函数可以获取传递的值。

5. 实际应用场景:HTTP 请求的 Context 使用

5.1 使用 Context 处理 HTTP 请求

package main


import (
  "fmt"
  ".NET/http"
  "time"
)


func handler(w http.ResponseWriter, r *http.Request) {
  ctx := r.Context()


  select {
  case <-time.After(3 * time.Second):
    fmt.Fprintln(w, "Hello, World!")
  case <-ctx.Done():
    err := ctx.Err()
    fmt.Println("处理请求:", err)
    http.Error(w, err.Error(), http.StatusInternalServerError)
  }
}


func main() {
  http.HandleFunc("/", handler)
  http.ListenAndServe(":8080", nil)
}

在上面例子中,使用http.Request结构体中的Context()方法获取到请求的context,并在处理请求时设置了 3 秒的超时时间。

如果请求在 3 秒内完成,就返回“Hello, World!”,否则返回一个错误。

5.2 处理 HTTP 请求的超时与取消

package main


import (
  "context"
  "fmt"
  "net/http"
  "time"
)


func handler(w http.ResponseWriter, r *http.Request) {
  ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second)
  defer cancel()


  select {
  case <-time.After(3 * time.Second):
    fmt.Fprintln(w, "Hello, World!")
  case <-ctx.Done():
    err := ctx.Err()
    fmt.Println("处理请求:", err)
    http.Error(w, err.Error(), http.StatusInternalServerError)
  }
}


func main() {
  http.HandleFunc("/", handler)
  http.ListenAndServe(":8080", nil)
}

在上面例子中,使用context.WithTimeout(parent, duration)函数为每个请求设置了 2 秒的超时时间。

如果请求在 2 秒内完成,就返回“Hello, World!”,否则返回一个错误。

6. 数据库操作中的 Context 应用

6.1 使用 Context 控制数据库查询的超时

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()


  ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
  
  defer cancel()


  rows, err := db.QueryContext(ctx, "SELECT * FROM users")
  
  if err != nil {
    fmt.Println("查询出错:", err)
    return
  }
  
  defer rows.Close()


  // 处理查询结果
}

在上面例子中,使用context.WithTimeout(parent, duration)函数为数据库查询设置了 2 秒的超时时间,确保在 2 秒内完成查询操作。

如果查询超时,程序会收到context的取消信号,从而中断数据库查询。

6.2 使用 Context 取消长时间运行的数据库事务

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()


  tx, err := db.Begin()
  if err != nil {
    fmt.Println(err)
    return
  }


  ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
  
  defer cancel()


  go func() {
    <-ctx.Done()
    
    fmt.Println("接收到取消信号,回滚事务")
    
    tx.Rollback()
  }()


  // 执行长时间运行的事务操作
  // ...


  err = tx.Commit()
  if err != nil {
  
    fmt.Println("提交事务出错:", err)
    
    return
  }


  fmt.Println("事务提交成功")
}

在这个例子中,使用context.WithTimeout(parent, duration)函数为数据库事务设置了 2 秒的超时时间。

同时使用 goroutine 监听context的取消信号。

在 2 秒内事务没有完成,程序会收到context的取消信号,从而回滚事务。

7. 自定义 Context 的实现

7.1 实现自定义的 Context 类型

package main


import (
  "context"
  "fmt"
  "time"
)


type MyContext struct {
  context.Context
  RequestID string
}


func WithRequestID(ctx context.Context, requestID string) *MyContext {
  return &MyContext{
    Context:   ctx,
    RequestID: requestID,
  }
}


func (c *MyContext) Deadline() (deadline time.Time, ok bool) {
  return c.Context.Deadline()
}


func (c *MyContext) Done() <-chan struct{} {
  return c.Context.Done()
}


func (c *MyContext) Err() error {
  return c.Context.Err()
}


func (c *MyContext) Value(key interface{}) interface{} {
  if key == "requestID" {
    return c.RequestID
  }
  return c.Context.Value(key)
}


func main() {
  ctx := context.Background()
  
  ctxWithRequestID := WithRequestID(ctx, "12345")


  requestID := ctxWithRequestID.Value("requestID").(string)
  
  fmt.Println("Request ID:", requestID)
}

在这个例子中,实现了一个自定义的MyContext类型,它包含了一个RequestID字段。

通过WithRequestID函数,可以在原有的context上附加一个RequestID值,然后在需要的时候获取这个值。

7.2 自定义 Context 的应用场景

自定义context类型的应用场景非常广泛,比如在微服务架构中,

可在context中加入一些额外的信息,比如用户 ID、请求来源等,以便在服务调用链路中传递这些信息。

8. Context 的生命周期管理

8.1 Context 的生命周期

context.Context 是不可变的,一旦创建就不能修改。它的值在传递时是安全的,可以被多个 goroutine 共享。

在一个请求处理的生命周期内,通常会创建一个顶级的 Context,然后从这个顶级的 Context 派生出子 Context,传递给具体的处理函数。

8.2 Context 的取消

当请求处理完成或者发生错误时,应该主动调用 context.WithCancel 或 context.WithTimeout 等函数创建的 Context 的 Cancel 函数来通知子 goroutines 停止工作。

这样可以确保资源被及时释放,避免 goroutine 泄漏。

func handleRequest(ctx context.Context) {


    // 派生一个新的 Context,设置超时时间
    ctx, cancel := context.WithTimeout(ctx, time.Second)
    
    defer cancel() // 确保在函数退出时调用 cancel,防止资源泄漏


    // 在这个新的 Context 下进行操作
    // ...
}

8.3 Context 的传递

在函数之间传递 Context 时,确保传递的是必要的最小信息。避免在函数间传递整个 Context,而是选择传递 Context 的衍生物。

如 context.WithValue 创建的 Context,其中包含了特定的键值对信息。

func middleware(next http.Handler) http.Handler {


return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 从请求中获取特定的信息
userID := extractUserID(r)


// 将特定的信息存储到 Context 中
ctx := context.WithValue(r.Context(), userIDKey, userID)


// 将新的 Context 传递给下一个处理器
next.ServeHTTP(w, r.WithContext(ctx))


})
}

9. Context 的注意事项

9.1 不要在函数签名中传递 Context

避免在函数的参数列表中传递 context.Context,除非确实需要用到它。

如果函数的逻辑只需要使用 Context 的部分功能,那么最好只传递它需要的具体信息,而不是整个 Context。

// 不推荐的做法
func processRequest(ctx context.Context) {
    // ...
}


// 推荐的做法
func processRequest(userID int, timeout time.Duration) {
    // 使用 userID 和 timeout,而不是整个 Context
}

9.2 避免在结构体中滥用 Context

不要在结构体中保存 context.Context,因为它会增加结构体的复杂性。

若是需要在结构体中使用 Context 的值,最好将需要的值作为结构体的字段传递进来。

type MyStruct struct {
    // 不推荐的做法
    Ctx context.Context
    
    // 推荐的做法
    UserID int
}
关键词:Go语言      点击(5)
声明:本站部分内容来自互联网,如有版权侵犯或其他问题请与我们联系,我们将立即删除或处理。
▍相关推荐
更多Go语言相关>>>