Go语言教程之边写边学:标准库:Context包

上下文包最佳实践

在Go中使用上下文包有几个最佳实践:

  • 使用context.WithCancel,context.WithTimeout或context.WithDeadline创建带有超时或取消信号的上下文。
  • 始终将上下文作为第一个参数传递给可能需要很长时间才能完成的函数,例如网络请求或数据库查询。
  • 使用context.Value存储和检索与上下文关联的值,例如用户ID或请求ID。
  • 使用context.WithValue基于现有上下文创建新上下文,并将其他值与其关联。
  • 检查上下文的Done通道,查看它是否已被取消。
  • 在整个应用程序中使用上下文包来传播请求范围的值和取消信号,而不是使用全局变量或手动信号。
  • 避免使用context.Background(),因为它没有超时或取消信号,而是使用context.TODO() 表示上下文稍后将被调用方替换。
  • 不要将上下文存储在结构中,而是将它们作为参数传递给函数。
  • 始终检查上下文感知函数的错误返回值,以查看上下文是否被取消或超时。

 

Go中的上下文包用于跨API边界传递请求范围的值、取消信号和截止时间。它可用于存储元数据、取消信号、超时和其他请求范围的值。上下文包提供了一种取消长时间运行的操作以及跨API边界存储元数据的方法。它通常与http包一起使用,以管理HTTP请求的请求范围值和取消信号。

它允许您在多个函数调用和goroutine之间传播请求范围的值,从而更轻松地管理应用程序中的信息流。

context.Context是使用context.With*函数创建,例如context.WithValuecontext.WithCancelcontext.WithTimeout。这些函数返回新的上下文,携带指定值或信号的上下文值。

context.Context可以作为参数传递给需要访问请求范围值或侦听取消信号的函数和方法。然后,这些函数可以使用context.Value和context.Done访问上下文中存储的值和信号的done方法。

在需要取消长时间运行的操作或跨多个goroutine传播请求范围的值的情况下,上下文包特别有用。它通常用于服务器端编程和其他并发方案。

应始终将上下文作为第一个参数传递给执行可能被取消的工作的任何函数。

例如,HTTP服务器可以使用上下文在客户端断开连接时取消请求的工作,数据库包可以使用上下文来实现可取消的查询,等等。

context包定义了 Context 类型,该类型是一个Go接口,具有四个方法,分别名为Deadline()、Done()、Err() 和Value():

type Context interface {
    Deadline() (deadline time.Time, ok bool)
    Done() 
    Err() 
    Value(key) 
}

上下文接口定义的方法

方法描述
Value(key)返回对应的值
Done()此方法返回可用于接收取消通知的频道
Deadline()此方法返回time.Time, 表示请求的截止日期,如果没有指定截止日期,则布尔值为false。
Err()此方法返回一个错误,指示完成通道接收信号的原因。上下文包定义了两个可用于比较错误的变量:Canceled表示请求已取消,DeadlineExeeded表示截止日期已过。

用于创建上下文值的上下文包函数

方法描述
Background()此方法返回默认上下文,从中派生其他上下文。
WithCancel(ctx)此方法返回一个上下文和一个取消函数。
WithDeadline(ctx, time)此方法返回一个带有截止日期的上下文,该截止日期用time.Time表示值。
WithTimeout(ctx, duration)此方法返回一个带有截止日期的上下文,该截止日期用time.Time表示值。
WithValue(ctx, key, val)此方法返回一个包含指定键值对的上下文。

 

context.WithCancel

下面是一个示例,说明context.WithCancel可以在Go中使用:

package main

    import (
        "context"
        "fmt"
        "time"
    )
    
    func doWork(ctx context.Context) {
        for {
            select {
            case <-ctx.Done():
                fmt.Println("Work done!")
                return
            default:
                fmt.Println("Working...")
            }
        }
    }
    
    func main() {
        ctx, cancel := context.WithCancel(context.Background())
        defer cancel() // 推迟执行cancel
    
        go doWork(ctx)
    
        // Wait for a while before canceling the context
        select {
        	case <-ctx.Done():
        	case <-time.After(time.Second * 3):
            	cancel()
        }
    }

在此示例中,我们使用context创建一个新的上下文 ctx.WithCancel(context.background())中。context.Background() 函数返回一个空的context。context.WithCancel 返回一个新的上下文和一个取消函数。我们推迟取消函数,以便在主函数退出时调用它。在 doWork 函数中,它将检查上下文是否已完成,如果是,则返回该函数。

在main函数中,我们正在运行一个goroutine,并通过传递上下文来完成其中的工作。等待3秒后,main函数会通过调用cancel函数来取消上下文,这将使上下文的Done通道关闭。因此,doWork函数将从Done通道接收,打印 "Work done!" 并返回。

 

context.WithTimeout

在Go中,您可以使用context.WithTimeout函数创建一个新上下文,该上下文在指定的超时时间过后被取消。该函数采用两个参数:现有上下文和超时持续时间。

下面是如何使用上下文的示例WithTimeout创建在5秒后取消的上下文:

package main

    import (
        "context"
        "fmt"
        "time"
    )
    
    func main() {
        ctx := context.Background()
        ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
        defer cancel()
    
        // Do some work
        select {
        case <-ctx.Done():
            fmt.Println("Work completed")
        case <-time.After(10 * time.Second):
            fmt.Println("Work took longer than 10 seconds")
        }
    }

在此示例中,上下文是通过调用context.WithTimeout与后台上下文5秒的超时创建。该函数返回一个新上下文和一个用于取消上下文的函数。cancel函数在defer语句中调用,以确保在函数返回时取消上下文。select语句用于等待上下文完成或超时结束。

您还可以使用ctx检查上下文是否已完成。Done() 通道,您可以在select语句或循环中使用此通道来检查上下文是否完成,如果完成则意味着上下文已过期。

 

context.WithDeadline

在Go中,context.WithDeadline 函数创建具有关联截止时间的新上下文。截止日期是一个特定的时间点,在此时间点之后,上下文将被视为"死亡",任何相关工作都将被取消。该函数接受两个参数:现有上下文和截止时间。它返回一个新的上下文,该上下文将在指定的截止时间取消。

下面是一个示例:

package main

    import (
        "context"
        "fmt"
        "time"
    )
    
    func main() {
        ctx := context.Background()
        deadline := time.Now().Add(time.Second * 5)
        ctx, cancel := context.WithDeadline(ctx, deadline)
        defer cancel()
    
        select {
        case <-time.After(time.Second * 10):
            fmt.Println("overslept")
        case <-ctx.Done():
            fmt.Println(ctx.Err())
        }
    }

在此示例中,将创建一个背景上下文,然后设置未来5秒的截止时间。WithDeadline 函数用于基于背景上下文创建具有指定截止时间的新上下文。select语句用于等待上下文被取消或等待10秒过去。如果在10秒之前取消上下文,它将打印错误消息上下文截止时间超过,否则将打印"overslept"

 

使用Context的SQL查询超时

要在Golang中使用超时的SQL查询,您可以使用上下文包来设置查询执行的截止时间。首先,使用上下文创建具有超时的context.WithTimeout函数。然后,将上下文作为第一个参数传递给查询执行函数(例如db.QueryContext() 或db.ExecContext())。

下面是如何为SELECT查询设置1秒超时的示例:

package main

    import (
      "context"
      "database/sql"
      "fmt"
      "time"
    )
    
    func main() {
      // Open a connection to the database
      db, _ := sql.Open("driverName", "dataSourceName")
    
      // Create a context with a timeout of 1 second
      ctx, cancel := context.WithTimeout(context.Background(), time.Second)
      defer cancel()
    
      // Execute the query with the context
      rows, err := db.QueryContext(ctx, "SELECT * FROM table")
      if err != nil {
        fmt.Println(err)
      }
      defer rows.Close()
    
      // Handle the query results
      // ...
    }

 

使用上下文超时读取文件

在Go中,可以使用上下文包来设置读取文件的超时时间。下面是一个示例:

package main

    import (
        "context"
        "fmt"
        "io/ioutil"
        "time"
    )
    
    func main() {
        ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
        defer cancel()
    
        data, err := ioutil.ReadFile("example.txt", ctx)
        if err != nil {
            fmt.Println("Error:", err)
            return
        }
    
        fmt.Println(string(data))
    }

在此示例中,我们首先使用context创建一个超时为2秒的context.WithTimeout。然后,我们将此上下文传递给ioutil。ReadFile读取文件"example.txt"的内容。如果读取文件的时间超过2秒,则上下文的Done通道将关闭,并且ioutil.ReadFile返回错误。

 

将上下文用于HTTP

在Go中,您可以使用上下文包来设置HTTP请求的超时。下面是一个示例:

package main

    import (
        "context"
        "fmt"
        "io/ioutil"
        "net/http"
        "time"
    )
    
    func main() {
        ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
        defer cancel()
    
        req, err := http.NewRequest("GET", "https://example.com", nil)
        if err != nil {
            fmt.Println("Error:", err)
            return
        }
        req = req.WithContext(ctx)
    
        client := &http.Client{}
        resp, err := client.Do(req)
        if err != nil {
            fmt.Println("Error:", err)
            return
        }
        defer resp.Body.Close()
    
        body, err := ioutil.ReadAll(resp.Body)
        if err != nil {
            fmt.Println("Error:", err)
            return
        }
    
        fmt.Println(string(body))
    }

在此示例中,我们首先使用context创建一个超时为2秒的context.WithTimeout。然后,我们使用req.WithContext(ctx) 将此上下文附加到HTTP请求中。当我们使用http.Client.Do方法发出请求时,如果在收到响应之前关闭了上下文的Done通道,它将自动取消请求。

您还可以使用客户端库(如golang.org/x/net/context/ctxhttp)发出带有上下文的http请求,该库具有Get和Post方法,该方法将上下文作为第一个参数并返回响应和错误。

package main

  import (
      "context"
      "fmt"
      "io/ioutil"
      "net/http"
      "time"
  
      "golang.org/x/net/context/ctxhttp"
  )
  
  func main() {
      ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
      defer cancel()
  
      resp, err := ctxhttp.Get(ctx, nil, "https://example.com")
      if err != nil {
          fmt.Println("Error:", err)
          return
      }
      defer resp.Body.Close()
  
      body, err := ioutil.ReadAll(resp.Body)
      if err != nil {
          fmt.Println("Error:", err)
          return
      }
  
      fmt.Println(string(body))
  }

此示例使用ctxhttp.Get方法向"https://example.com"发出GET请求,超时为2秒。

请务必注意,在这两种情况下,如果上下文关闭了Done通道,则请求将被取消,但不会关闭连接。应用程序负责关闭连接。

 

使用Context作为键值存储

在Go中,您可以使用上下文包来存储可以与请求或一段代码一起传递的键值数据对。这允许您将其他信息与请求或代码段相关联,而不必将其作为显式参数传递。下面是一个示例:

package main

    import (
        "context"
        "fmt"
    )
    
    func main() {
        ctx := context.WithValue(context.Background(), "user_id", "12345")
        // use the context in a function
        processRequest(ctx)
    }
    
    func processRequest(ctx context.Context) {
        userID := ctx.Value("user_id").(string)
        fmt.Println("User ID:", userID)
    }

在此示例中,我们首先使用context创建一个上下文。WithValue方法。我们传递上下文。Background() 作为父上下文,以及方法的键值对 "user_id" 和 "12345"。然后,我们将此上下文传递给processRequest函数。在函数中,我们使用Value方法从上下文中检索"user_id"值,并将其打印出来。

请务必注意,上下文值仅用于传输进程和API边界的请求范围数据,而不用于将可选参数传递给函数。如果需要将可选参数传递给函数,最好使用结构或函数选项模式。

此外,上下文值不是线程安全的,如果您处于并发环境中,请使用sync.Map或同等产品。