Go中基于上下文的编程

在goroutine中运行多个并发计算的Go程序需要管理其生命周期。 失控的goroutine可能陷入无限循环,使其他正在等待的goroutine死锁,或者花费太长时间。 理想情况下,您应该能够取消goroutine或在经过一段时间后使它们超时。

输入基于内容的编程。 Go 1.7引入了上下文包,它提供了这些功能以及将任意值与上下文关联的能力,该上下文随请求的执行而传播,并允许带外通信和信息传递。

在本教程中,您将学习Go中上下文的来龙去脉,何时以及如何使用它们,以及如何避免滥用它们。

谁需要上下文?

上下文是非常有用的抽象。 它允许您封装与核心计算无关的信息,例如请求ID,授权令牌和超时。 这种封装有几个好处:

  • 它将核心计算参数与操作参数分开。
  • 它整理了常见的操作方面以及如何跨边界进行沟通。
  • 它提供了一种标准机制,可在不更改签名的情况下添加带外信息。

上下文界面

这是整个Context接口:

type Context interface {
    Deadline() (deadline time.Time, ok bool)
	Done() <-chan struct{}
	Err() error
	Value(key interface{}) interface{}

以下各节说明了每种方法的目的。

Deadline()方法

截止日期返回应取消代表该上下文完成的工作的时间。 如果未设置截止日期,则截止日期返回ok==false 。 连续调用Deadline会返回相同的结果。

Done()方法

Done()返回一个通道,当代表该上下文完成的工作应被取消时,该通道已关闭。 如果此上下文永远无法取消,则完成可能会返回nil。 连续调用Done()会返回相同的值。

  • context.WithCancel()函数安排在调用cancel时关闭Done通道。
  • context.WithDeadline()函数安排在截止日期到期时关闭Done通道。
  • context.WithTimeout()函数安排在超时结束后关闭Done通道。

可以在select语句中使用完成:

// Stream generates values with DoSomething and sends them 
 // to out until DoSomething returns an error or ctx.Done is
 // closed.
 func Stream(ctx context.Context, out chan<- Value) error {
     for {
 		v, err := DoSomething(ctx)
 		if err != nil {
 			return err
 		}
 		select {
 		case <-ctx.Done():
 			return ctx.Err()
 		case out <- v:
 		}
 	}
 }

有关如何使用“完成”渠道进行取消的更多示例,请参见Go博客中的本文

Err()方法

只要Done通道处于打开状态,Err()返回nil。 如果上下文Canceled则返回“ Canceled取消”,如果上下文的截止日期已过或超时已过期,则返回DeadlineExceeded 。 关闭Done之后,对Err()的连续调用将返回相同的值。 以下是定义:

// Canceled is the error returned by Context.Err when the 
// context is canceled.
var Canceled = errors.New("context canceled")

// DeadlineExceeded is the error returned by Context.Err 
// when the context's deadline passes.
var DeadlineExceeded error = deadlineExceededError{}

Value()方法

值返回与此键的上下文关联的值,如果没有值与键关联,则返回nil。 使用相同的键连续调用Value会返回相同的结果。

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

键标识上下文中的特定值。 希望在Context中存储值的函数通常在全局变量中分配一个键,并将该键用作context.WithValue()和Context.Value()的参数。 键可以是支持相等性的任何类型。

上下文范围

上下文具有范围。 您可以从其他范围派生范围,并且父范围不能访问派生范围中的值,但是派生范围可以访问父范围的值。

上下文形成层次结构。 您从context.Background()或context.TODO()开始。 每当调用WithCancel(),WithDeadline()或WithTimeout()时,都将创建派生上下文并接收取消功能。 重要的是,当父级上下文被取消或过期时,其所有派生上下文。

您应该在main()函数,init()函数和测试中使用context.Background()。 如果不确定要使用的上下文,则应使用context.TODO()。

请注意,Background和TODO 不可取消。

截止日期,超时和取消

您还记得,WithDeadline()和WithTimeout()返回的上下文会自动被取消,而WithCancel()返回的上下文必须被显式取消。 它们都返回取消功能,因此即使超时/截止时间尚未到期,您仍可以取消任何派生的上下文。

让我们来看一个例子。 首先,这是带有名称和上下文的contextDemo()函数。 它无限循环运行,将其名称和上下文的截止日期打印到控制台(如果有)。 然后它睡了一秒钟。

package main 

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

func contextDemo(name string, ctx context.Context) {    
    for {
        if ok {
            fmt.Println(name, "will expire at:", deadline)
        } else {
            fmt.Println(name, "has no deadline")
        }
        time.Sleep(time.Second)
    }
}

主要功能创建三个上下文:

  • 具有三秒超时的timeoutContext
  • 非过期的cancelContext
  • 最后一个截止时间,它是从cancelContext派生的,截止日期为现在的四个小时

然后,它将启动contextDemo函数作为三个goroutine。 所有这些并发运行,并每秒打印一次消息。

然后,主函数通过从其Done()通道中读取来等待带有timeoutCancel的goroutine被取消(将阻塞直到关闭)。 一旦超时在三秒钟后到期,main()就会调用cancelFunc(),它会使用cancelContext取消goroutine,并使用派生的四个小时的截止时间上下文取消最后一个goroutine。

func main() {
    timeout := 3 * time.Second
    deadline := time.Now().Add(4 * time.Hour)
    timeOutContext, _ := context.WithTimeout(
        context.Background(), timeout)
    cancelContext, cancelFunc := context.WithCancel(
        context.Background())
    deadlineContext, _    := context.WithDeadline(
        cancelContext, deadline)


    go contextDemo("[timeoutContext]", timeOutContext)
    go contextDemo("[cancelContext]", cancelContext)
    go contextDemo("[deadlineContext]", deadlineContext)

    // Wait for the timeout to expire
    <- timeOutContext.Done()

    // This will cancel the deadline context as well as its
    // child - the cancelContext
    fmt.Println("Cancelling the cancel context...")
    cancelFunc()

    <- cancelContext.Done()
    fmt.Println("The cancel context has been cancelled...")

    // Wait for both contexts to be cancelled
    <- deadlineContext.Done()
    fmt.Println("The deadline context has been cancelled...")
}

这是输出:

[cancelContext] has no deadline
[deadlineContext] will expire at: 2017-07-29 09:06:02.34260363
[timeoutContext] will expire at: 2017-07-29 05:06:05.342603759
[cancelContext] has no deadline
[timeoutContext] will expire at: 2017-07-29 05:06:05.342603759
[deadlineContext] will expire at: 2017-07-29 09:06:02.34260363
[cancelContext] has no deadline
[timeoutContext] will expire at: 2017-07-29 05:06:05.342603759
[deadlineContext] will expire at: 2017-07-29 09:06:02.34260363
Cancelling the cancel context...
The cancel context has been cancelled...
The deadline context has been cancelled...

在上下文中传递值

您可以使用WithValue()函数将值附加到上下文。 请注意,返回的是原始上下文, 而不是派生上下文。 您可以使用Value()方法从上下文中读取值。 让我们修改演示函数以从上下文中获取其名称,而不是将其作为参数传递:

func contextDemo(ctx context.Context) {
    deadline, ok := ctx.Deadline()
    name := ctx.Value("name")
    for {
        if ok {
            fmt.Println(name, "will expire at:", deadline)
        } else {
            fmt.Println(name, "has no deadline")
        }
        time.Sleep(time.Second)
    }
}

然后修改主函数以通过WithValue()附加名称:

go contextDemo(context.WithValue(
    timeOutContext, "name", "[timeoutContext]"))
go contextDemo(context.WithValue(
    cancelContext, "name", "[cancelContext]"))
go contextDemo(context.WithValue(
    deadlineContext, "name", "[deadlineContext]"))

输出保持不变。 有关适当使用上下文值的一些准则,请参见最佳做法部分。

最佳实践

围绕上下文值出现了一些最佳实践:

  • 避免在上下文值中传递函数参数。
  • 希望在Context中存储值的函数通常在全局变量中分配键。
  • 软件包应将键定义为未导出的类型,以免发生冲突。
  • 定义上下文关键字的包应为使用该关键字存储的值提供类型安全的访问器。

HTTP请求上下文

上下文最有用的用例之一是将信息与HTTP请求一起传递。 该信息可能包括请求ID,身份验证凭据等。 在Go 1.7中,标准的net / http包利用了上下文包“标准化”的优势,并直接向请求对象添加了上下文支持:

func (r *Request) Context() context.Context
func (r *Request) WithContext(ctx context.Context) *Request

现在,可以以标准方式将标头中的请求ID一直附加到最终处理程序。 WithRequestID()处理函数从“ X-Request-ID”标头中提取请求ID,并使用其使用的现有上下文生成具有请求ID的新上下文。 然后将其传递给链中的下一个处理程序。 GetRequestID()公共函数提供对可能在其他程序包中定义的处理程序的访问。

const requestIDKey int = 0


func WithRequestID(next http.Handler) http.Handler {
    return http.HandlerFunc(
        func(rw http.ResponseWriter, req *http.Request) {
            // Extract request ID from request header
            reqID := req.Header.Get("X-Request-ID")
            // Create new context from request context with 
            // the request ID
            ctx := context.WithValue(
                req.Context(), requestIDKey, reqID)
            // Create new request with the new context
            req = req.WithContext(ctx)
            // Let the next handler in the chain take over.
            next.ServeHTTP(rw, req)
        }
    )
}

func GetRequestID(ctx context.Context) string {
    ctx.Value(requestIDKey).(string)
}


func Handle(rw http.ResponseWriter, req *http.Request) {
    reqID := GetRequestID(req.Context())
    ...
}


func main() {
    handler := WithRequestID(http.HandlerFunc(Handle))
    http.ListenAndServe("/", handler)
}

结论

基于上下文的编程为解决两个常见问题提供了一种标准且得到良好支持的方法:管理goroutine的生存期以及跨功能链传递带外信息。

遵循最佳实践并在正确的上下文中使用上下文(请参阅我在那做的事情?),您的代码将得到很大的改善。

翻译自: https://code.tutsplus.com/tutorials/context-based-programming-in-go--cms-29290

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值