返回
顶部

references:

介绍

在go服务中,每一个进来的请求都由自己的goroutine来负责处理,请求处理器经常启动额外的goroutine来访问后端服务,比如服务器或者RPC服务

一般情况下,goroutine在请求处理的时候需要访问该请求相关的一些特定数据,比如身份信息、认证token和请求时效,当请求取消或者超时之后,所有处理该请求的goroutine都应该快速退出,以便系统能够尽快地回收他们所占用的资源

Google开发了一个context包以便更简单便捷地传递只在该请求中有效的一些数据,context可以打破goroutine的API边界,以在更大的范围内共享数据,本文将会提供完整的代码样例以演示context的使用

context

context包的核心就是Context类型,其定义如下:

// A Context carries a deadline, cancellation signal, and request-scoped values
// across API boundaries. Its methods are safe for simultaneous use by multiple
// goroutines.
type Context interface {
    // Done returns a channel that is closed when this Context is canceled
    // or times out.
    Done() <-chan struct{}

    // Err indicates why this context was canceled, after the Done channel
    // is closed.
    Err() error

    // Deadline returns the time when this Context will be canceled, if any.
    Deadline() (deadline time.Time, ok bool)

    // Value returns the value associated with key or nil if none.
    Value(key interface{}) interface{}
}

Context没有Cancel函数,和Done频道是只接收的一个原因,会收到取消信号的函数一般都不会发送信号

Context对象是并发安全的,可以将Context传递给多个goroutine,取消这个Context即可通知所有的goroutine

Deadline函数用于判断是否应该开始工作,如果剩余时间太短,那么就不值当再去执行了,实际的代码可能会使用deadline来为IO操作设置超时时间

Value允许Context携带请求相关的数据,针对这些数据的操作必须为并发安全的

派生context

context包提供了一个函数可以用于从已经存在的context中派生新的context值,这些值来自于一棵树,当一个Context被取消之后,所有的Context派生的context也都会被取消

Background函数是Context树的root,他永远不会被取消

// Background returns an empty Context. It is never canceled, has no deadline,
// and has no values. Background is typically used in main, init, and tests,
// and as the top-level Context for incoming requests.
func Background() Context

WithCancel和WithTimeout返回派生的Context值,他可以早于父Context被取消,和请求关联的Context通常会在handler返回之后被取消

WithCancel is also useful for canceling redundant requests when using multiple replicas(没看懂)

WithTimeout可以设置deadline

WithValue提供了一种将请求范围内有效的值和Context关联起来的手段

// WithValue returns a copy of parent whose Value method returns val for key.
func WithValue(parent Context, key interface{}, val interface{}) Context

例子,Google web search

本例为一个HTTP server,他会处理这样的URL:

/search?q=golang&timeout=1s

他会将这个“golang”请求转发给Google Web Search API并渲染结果,timeout参数告诉服务器在这个时间之后取消该请求

代码被划分到3个包里面:

  • server包,提供main方法,以及/search的处理代码
  • userip包,从请求中提取用户IP地址并将其关联到Context中
  • google包,提供search函数,将请求发送给Google

服务端程序

服务器处理/search?q=golang这样的请求,他注册了handleSearch函数来处理/search端点,处理代码创建一个初始Context,叫做ctx并将其设置为在处理代码返回之后取消

// 使用http库的HandleFunc注册/search模式的路径到handleSearch函数
http.HandleFunc("/search", handleSearch)

如果设置了超时参数就在超时后自动取消

timeout, err := time.ParseDuration(req.FormValue("timeout"))
if err == nil {
    // The request has a timeout, so create a context that is
    // canceled automatically when the timeout expires.
    ctx, cancel = context.WithTimeout(context.Background(), timeout)
} else {
    ctx, cancel = context.WithCancel(context.Background())
}

context.WithTime和context.WithCancel都会返回一个从context.Background()(根Context)派生出来的子context

前者会在超时后自动取消,也可以使用返回的cancel函数主动取消

后者只能使用cancel函数主动取消

下一行代码是defer cancel() // Cancel ctx as soon as handleSearch returns.

这行代码可以保证在该函数返回之前调用cancel函数来取消context,避免资源的泄漏

然后我们需要获取到用户的IP并将其存储在ctx中

userIP, err := userip.FromRequest(req)
if err != nil {
    http.Error(w, err.Error(), http.StatusBadRequest)
    return
}
ctx = userip.NewContext(ctx, userIP)

NewContext使用如下代码存储数据

    return context.WithValue(ctx, userIPKey, userIP)

需要我们自定义一个key

const userIPKey key = 0

key的值我们可以随便写,别在同一个context里面冲突就行

context.WithValue会返回一个新的context,原来的context就变成了新context的父亲,构建在树上,如下所示

context.Background()  root节点
context  儿子节点
新context 孙子节点

当我们访问新context的value时:ctx.Value(key),那么他就会先查询新context本身是否有这个key,如果没有就往上遍历,直到root节点,也就是说,原来的context被嵌入到了新context中

req = req.WithContext(ctx)
go func() { c <- f(http.DefaultClient.Do(req)) }()

执行完WithContext函数之后,如果ctx中一开始设置了timeout,则http.DefaultClient.Do会在超时的时候返回

context的特别之处

考虑下面这个例子:

package main

import (
    "fmt"
    "time"
)

type StopSignal struct {
    Stop bool
}

func worker(id int, sig *StopSignal) {
    for i := 0; i < 10; i++ {
        if sig.Stop {
            fmt.Printf("worker %d stopped manually\n", id)
            return
        }
        fmt.Printf("worker %d working %d\n", id, i)
        time.Sleep(200 * time.Millisecond)
    }
}

func main() {
    sig := &StopSignal{}

    for i := 1; i <= 3; i++ {
        go worker(i, sig)
    }

    time.Sleep(1 * time.Second)
    sig.Stop = true // stop signal

    time.Sleep(500 * time.Millisecond)
    fmt.Println("main done")
}

我们所创建的3个goroutine全部通过循环检查sig变量来确定自己什么时候应该退出,而如果我们引入context,就会变得简单和高效

package main

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

func worker(ctx context.Context, id int) {
    for i := 0; i < 10; i++ {
        select {
        case <-ctx.Done():
            fmt.Printf("worker %d stopped: %v\n", id, ctx.Err())
            return
        default:
            fmt.Printf("worker %d working %d\n", id, i)
            time.Sleep(200 * time.Millisecond)
        }
    }
}

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

    for i := 1; i <= 3; i++ {
        go worker(ctx, i)
    }

    // no need to manually signal stop
    time.Sleep(2 * time.Second)
    fmt.Println("main done")
}

所有goroutine会在超时后自动退出

也可以手动取消

package main

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

func worker(ctx context.Context, id int) {
    for i := 0; i < 1000; i++ {
        select {
        case <-ctx.Done():
            fmt.Printf("worker %d stopped: %v\n", id, ctx.Err())
            return
        default:
            fmt.Printf("worker %d working %d\n", id, i)
            time.Sleep(200 * time.Millisecond)
        }
    }
}

func main() {
    var (
        ctx    context.Context
        cancel context.CancelFunc
    )
    ctx, cancel = context.WithCancel(context.Background())

    for i := 1; i <= 3; i++ {
        go worker(ctx, i)
    }

    time.Sleep(2 * time.Second)
    cancel()
    // no need to manually signal stop
    time.Sleep(100 * time.Second)
    fmt.Println("main done")
}