登录
首页 >  Golang >  Go教程

一篇文章搞懂Go语言中的Context

来源:脚本之家

时间:2023-01-01 19:27:16 418浏览 收藏

本篇文章主要是结合我之前面试的各种经历和实战开发中遇到的问题解决经验整理的,希望这篇《一篇文章搞懂Go语言中的Context》对你有很大帮助!欢迎收藏,分享给更多的需要的朋友学习~

0 前置知识sync.WaitGroup

sync.WaitGroup是等待一组协程结束。它实现了一个类似任务队列的结构,可以向队列中加入任务,任务完成后就把任务从队列中移除,如果队列中的任务没有全部完成,队列就会触发阻塞以阻止程序继续运行。 sync.WaitGroup只有3个方法,Add(),Done(),Wait() 。

其中Done()是Add(-1)的别名,使用Add()添加计数,Done()减掉一个计数,计数不为0, 阻塞Wait()的运行。

示例:

package main
import (
   "fmt"
   "sync"
   "time"
)
var group sync.WaitGroup
func sayHello() {
   for i := 0; i 

效果:

1 简介

在 Go 服务器中,每个传入请求都在其自己的 goroutine 中处理。请求处理程序通常会启动额外的 goroutine 来访问后端,例如数据库和 RPC 服务。处理请求的一组 goroutine 通常需要访问特定于请求的值,例如最终用户的身份、授权令牌和请求的截止日期。当请求被取消或超时时,处理该请求的所有 goroutine 都应该快速退出,以便系统可以回收它们正在使用的任何资源。

为此,开发了一个context包,可以轻松地将请求范围的值、取消信号和截止日期跨 API 边界传递给处理请求所涉及的所有 goroutine。

Context携带一个截止日期、一个取消信号和其他跨越API边界的值。上下文的方法可以被多个gor例程同时调用。

对服务器的传入请求应该创建一个上下文,对服务器的传出调用应该接受一个上下文。它们之间的函数调用链必须传播 Context,可选择将其替换为使用 WithCancel、WithDeadline、WithTimeout 或 WithValue 创建的派生 Context。当一个上下文被取消时,所有从它派生的上下文也被取消。

WithCancel、WithDeadline 和 WithTimeout 函数采用 Context(父)并返回派生的 Context(子)和 CancelFunc。调用 CancelFunc 会取消子项及其子项,删除父项对子项的引用,并停止任何关联的计时器。调用 CancelFunc 失败会泄漏子项及其子项,直到父项被取消或计时器触发。go vet 工具检查是否在所有控制流路径上使用了 CancelFuncs。

使用上下文的程序应遵循以下规则,以保持跨包的接口一致,并启用静态分析工具来检查上下文传播:

不要将上下文存储在结构类型中;相反,将 Context 显式传递给需要它的每个函数。

Context 应该是第一个参数,通常命名为 ctx:

func DoSomething(ctx context.Context, arg Arg) error { 
    // ... 使用 ctx ... 
}

即使函数允许,也不要传递 nil 上下文。如果不确定要使用哪个 Context,请传递 context.TODO。

仅将上下文值用于传输流程和 API 的请求范围数据,而不用于将可选参数传递给函数。

相同的 Context 可以传递给在不同的 goroutine 中运行的函数;上下文对于多个 goroutine 同时使用是安全的。

2 context.Context引入

//上下文携带截止日期、取消信号和请求范围的值在API的界限。它的方法是安全的同时使用多个了goroutine。
type Context interface {
    // Done返回一个在上下文被取消或超时时关闭的通道。
    Done() 
  • Done方法返回一个通道,该通道作为代表运行的函数的取消信号Context:当通道关闭时,函数应该放弃它们的工作并返回。
  • Err方法返回一个错误,指示Context取消的原因。
  • 一个Context对于多个 goroutine 同时使用是安全的。代码可以将单个传递Context给任意数量的 goroutines 并取消它Context以向所有goroutine 发出信号。
  • Deadline方法允许函数确定它们是否应该开始工作,还可以使用截止日期来设置 I/O 操作的超时时间。
  • Value允许一个Context携带请求范围的数据。该数据必须是安全的,以便多个 goroutine 同时使用。

3 context包的其他常用函数

3.1 context.Background和context.TODO

Background是任何Context树的根,它永远不会被取消:

//Background返回一个空的Context。 它永远不会取消,没有截止日期,没有价值。 Background通常用于main、init和tests,并作为传入请求的顶级上下文。  
func Background() Context

给一个函数方法传递Context的时候,不要传递nil,如果不知道传递什么,就使用context.TODO()

3.2 context.WithCancel和

WithCancelt返回派生的Context值,可以比父Context更快地取消。当请求处理程序返回时,通常会取消与传入请求关联的content。当使用多个副本时,WithCancel对于取消冗余请求也很有用。

// WithCancel返回一个父进程的副本,该父进程的Done通道被尽快关闭。 关闭Done或调用cancel。
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
// CancelFunc取消一个上下文。
type CancelFunc func()

示例:

package main
import (
   "context"
   "fmt"
)
func play(ctx context.Context) 

扩展:go中select的用法

```
select的用法与switch语言非常类似,由select开始一个新的选择块,每个选择条件由case语句来描述。
与switch语句相比, select有比较多的限制,其中最大的一条限制就是每个case语句里必须是一个IO操作,大致的结构如下:
``` go
select {
   case 

3.3 context.WithTimeout

WithTimeout返回派生的Context值,WithTimeout用于设置请求到后端服务器的截止日期:

//WithTimeout返回一个父进程的副本,该父进程的Done通道被立即关闭的父母。关闭“完成”、调用“取消”或超时结束。新
//Context的Deadline是现在的更快+timeout和父的Deadline,如果任何。 如果计时器仍然在运行,则cancel函数释放它资源。
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
// CancelFunc取消一个上下文。
type CancelFunc func()

示例:

package main
import (
   "context"
   "fmt"
   "sync"
   "time"
)
var wg sync.WaitGroup
func worker(ctx context.Context) {
    LOOP:
   for {
      fmt.Println("db connecting ...")
      time.Sleep(time.Millisecond * 10) // 假设正常连接数据库耗时10毫秒
      select {
      case 

执行结果:

3.4 context.WithDeadline

func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
   if parent == nil {
      panic("cannot create context from nil parent")
   }
   if cur, ok := parent.Deadline(); ok && cur.Before(d) {
      // 目前的期限已经比新的期限提前
      return WithCancel(parent)
   }
   c := &timerCtx{
      cancelCtx: newCancelCtx(parent),
      deadline:  d,
   }
   propagateCancel(parent, c)
   dur := time.Until(d)
   if dur 

示例:

package main
import (
   "context"
   "fmt"
   "time"
)
func main() {
   d := time.Now().Add(500 * time.Millisecond)
   ctx, cancel := context.WithDeadline(context.Background(), d)
   // 尽管ctx会过期,但在任何情况下调用它的cancel函数都是很好的实践。
   // 如果不这样做,可能会使上下文及其父类存活的时间超过必要的时间。
   defer cancel()
   select {
   case 

执行结果:

3.5 context.WithValue

WithValue提供了一种将请求范围的值与Context关联的方法 :

//WithValue返回父元素的副本,其Value方法返回val for key。
func WithValue(parent Context, key interface{}, val interface{}) Context

了解如何使用context包的最好方法是通过一个已工作的示例。

示例:

package main
import (
   "context"
   "fmt"
   "sync"
   "time"
)
type TraceCode string
var wg sync.WaitGroup
func worker(ctx context.Context) {
   key := TraceCode("KEY_CODE")
   traceCode, ok := ctx.Value(key).(string) // 在子goroutine中获取trace code
   if !ok {
      fmt.Println("invalid trace code")
   }
    LOOP:
   for {
      fmt.Printf("worker,code:%s\n", traceCode)
      time.Sleep(time.Millisecond * 10) // 假设正常连接数据库耗时10毫秒
      select {
      case 

执行结果:

4 实例:请求浏览器超时

server端:

package main
import (
   "fmt"
   "math/rand"
   "net/http"
   "time"
)
// server端,随机出现慢响应
func indexHandler(w http.ResponseWriter, r *http.Request) {
   number := rand.Intn(2)
   if number == 0 {
      time.Sleep(time.Second * 10) // 耗时10秒的慢响应
      fmt.Fprintf(w, "slow response")
      return
   }
   fmt.Fprint(w, "quick response")
}
func main() {
   http.HandleFunc("/", indexHandler)
   err := http.ListenAndServe(":9999", nil)
   if err != nil {
      panic(err)
   }
}

client端:

package main
import (
   "context"
   "fmt"
   "io/ioutil"
   "net/http"
   "sync"
   "time"
)
// 客户端
​
type respData struct {
   resp *http.Response
   err  error
}
func doCall(ctx context.Context) {
   // http长连接
   transport := http.Transport{DisableKeepAlives: true}
   client := http.Client{Transport: &transport}
​
   respChan := make(chan *respData, 1)
   req, err := http.NewRequest("GET", "http://127.0.0.1:9999/", nil)
   if err != nil {
      fmt.Println(err)
      return
   }
   req = req.WithContext(ctx) // 使用带超时的ctx创建一个新的client request
   var wg sync.WaitGroup
   wg.Add(1)
   defer wg.Wait()
   go func() {
      resp, err := client.Do(req)
      fmt.Printf("resp:%v, err:%v\n", resp, err)
      rd := &respData{
         resp: resp,
         err:  err,
      }
      respChan 

5 Context包都在哪些地方使用

许多服务器框架提供了用于承载请求作用域值的包和类型。我们可以定义“Context”接口的新实现,在使用现有框架的代码和需要“Context”参数的代码之间架起桥梁。

6 小结

在谷歌中,要求Go程序员将“Context”参数作为传入和传出请求之间的调用路径上的每个函数的第一个参数传递。这使得许多不同团队开发的Go代码能够很好地互操作。它提供了对超时和取消的简单控制,并确保像安全凭证这样的关键值能够正确地传输Go程序。

想要构建在“Context”上的服务器框架应该提供“Context”的实现来连接它们的包和那些需要“Context”参数的包。然后,它们的客户端库将接受来自调用代码的“Context”。通过为请求范围的数据和取消建立一个公共接口,“上下文”使包开发人员更容易共享创建可伸缩服务的代码。

好了,本文到此结束,带大家了解了《一篇文章搞懂Go语言中的Context》,希望本文对你有所帮助!关注golang学习网公众号,给大家分享更多Golang知识!

声明:本文转载于:脚本之家 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>
评论列表