一、问题的提出:
在 Go http包的Server中,
也就是在go的http服务器中,对每一个请求request都会有一个对应的 goroutine 去处理该请求,而且对应的请求处理函数在处理该请求时有可能又会启动新的的 goroutine 用来访问一些后端服务:比如数据库和RPC服务。
而且用来处理同一个请求request的 各goroutine间 通常都需要访问与该request相关的数据,比如终端用户的身份认证信息、验证相关的token、请求的截止时间,那么如何方便的在这些goroutine间(也就是在与处理该request相关的各goroutine间)传递该request域的数据,以及 如何在该request被取消或超时时,及时的关闭所有用来处理该请求的 goroutine?原则上当一个请求被取消或超时时,所有用来处理该请求的 goroutine 都应该迅速退出,然后系统才能释放这些 goroutine 占用的资源。
二、解决方案:
Go 的Context库就是用来解决以上问题的。Go1.7加入了一个新的标准库context,它定义了Context类型。
1、通过
func WithValue(parent Context, key, val interface{}) Context
实现request域数据与Context对象的绑定,并且可以通过在不同的goroutine间传递该Context对象或者其派生Context对象来解决:在这些goroutine间(也就是在与处理该request相关的各goroutine间)传递该request域的数据的问题,且各goroutine队该context对象的值操作是并发安全的!!!
2、可以通过往子goroutine或者处理同一个request请求的各goroutine传递一个Context对象,然后通过该传递来的Context的Done通道中是否有值来判定当前任务是否被Cancel或者执行超时了 。这个Done通道即为Done()方法返回的channel通道,当context因超时或者到了截止时间或者被手动cancel时,会自动往其Done通道中存一个值,反言之只要能从Context的Done通道中取出一个值就表示该Context被cancel或者中止了,也就是表示该goroutine的任务被取消了其应被及时关闭,释放其所占的资源。
三、 Context接口
context.Context是一个接口,该接口定义了四个需要实现的方法
。
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key interface{}) interface{}
}
其中:
-
Deadline()方法需要返回当前Context被取消的时间,也就是完成工作的截止时间(deadline);
-
Done()方法需要返回一个只读Channel,这个Channel会在当前工作完成或者上下文被取消之后关闭,多次调用Done()方法也只会返回同一个Channel;
-
Err()方法会返回当前Context结束的原因,它只会在Done返回的Channel被关闭时才会返回非空的值也就是返回一个err告知当前Context结束的原因:
1)如果当前Context被取消就会返回Canceled错误;
2)如果当前Context超时就会返回DeadlineExceeded错误;
-
Value()方法会从Context中返回键对应的值,对于同一个上下文来说,多次调用Value并传入相同的Key会返回相同的结果,该方法仅用于传递跨API和进程间跟请求域的数据;
【注意】:只要父Context对象关闭了,那么其所有的子(派生)Context对象也会被关闭!!!
四、生成顶层(根)Context对象:Background()和TODO()
context库内置了两个函数:Background()和TODO(),这两个函数均是返回一个空的Context对象(分别对应变量background以及todo);我们代码中最开始都是以这两个内置的上下文对象作为最顶层的partent context,衍生出更多的子上下文对象。
它们的使用区别如下:
-
Background()主要用于main函数、初始化以及测试代码中,作为Context这个树结构的最顶层的Context,也就是根Context。
-
TODO():如果我们不知道该使用什么Context的时候,可以使用这个。
background和todo本质上都是一个实现了Context接口的emptyCtx类型(定义见下),是一个不可取消,没有设置截止时间,没有携带任何值的Context。
// An emptyCtx is never canceled, has no values, and has no deadline. It is not
// struct{}, since vars of this type must have distinct addresses.
type emptyCtx int
func (*emptyCtx) Deadline() (deadline time.Time, ok bool) {
return
}
func (*emptyCtx) Done() <-chan struct{} {
return nil
}
func (*emptyCtx) Err() error {
return nil
}
func (*emptyCtx) Value(key interface{}) interface{} {
return nil
}
func (e *emptyCtx) String() string {
switch e {
case background:
return "context.Background"
case todo:
return "context.TODO"
}
return "unknown empty Context"
}
五、创建当前(父)Context的派生(子)Context对象的四个WithXXX()方法
1、WithCancel()
:
-
创建一个可手动cancel的parent Context对象的派生context对象(或者叫parent Context的副本)以及一个用于手动cancel该派生context的回调函数cancelFunc()
-
当我们手动调用返回的cancel函数或者当关闭父Context(的Done通道)时,将关闭返回的派生Context 对象(的Done通道,这个Done通道即为Done()返回的channel通道),同时往该 Done通道内存入一个值。此时如果能从该Context的Done通道内取出一值即为表示任务被cancel或者执行超时了,应立即释放与该context有关的资源,并关闭接受该context的goroutine(因为其运行的任务被cancel或者因为执行超时被迫关闭),释放其所占的资源
方法签名:
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
示例:
func gen(ctx context.Context) <-chan int {
dst := make(chan int)
n := 1
go func() {
for {
select {
case <-ctx.Done():
return // return结束该goroutine,防止泄露
case dst <- n:
n++
}
}
}()
return dst
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel() // 当我们取完需要的整数后调用cancel
for n := range gen(ctx) {
fmt.Println(n)
if n == 5 {
break
}
}
}
//上面的示例代码中,gen函数在单独的goroutine中生成整数并将它们发送到返回的通道。 gen的调用者在使用生成的整数之后需要取消上下文,以免gen启动的内部goroutine发生泄漏。
2、WithDeadline()
-
创建一个可以指定截止时间deadline的该传入context对象的派生context对象,以及一个用于手动cancel该派生context的回调函数cancelFunc()
-
当到截止日过期deadline时,或者当调用返回的cancel函数时,或者当parent Context(的Done通道)关闭时,返回的派生Context对象(的Done通道)将被关闭,同时往该 Done通道内存入一个值。此时如果能从该Context的Done通道内取出一值即为表示任务被cancel或者执行超时了,应立即释放与该context有关的资源,并关闭该goroutine的运行,释放其所占的资源
方法签名
:
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)
示例:
func main() {
d := time.Now().Add(50 * time.Millisecond)
ctx, cancel := context.WithDeadline(context.Background(), d)
// 尽管ctx会过期,但在任何情况下调用它的cancel函数都是很好的实践。
// 如果不这样做,可能会使上下文及其父类存活的时间超过必要的时间。
defer cancel()
select {
case <-time.After(1 * time.Second):
fmt.Println("overslept")
case <-ctx.Done():
fmt.Println(ctx.Err())
}
}
上面的代码中,定义了一个50毫秒之后过期的deadline,然后我们调用context.WithDeadline(context.Background(), d)得到一个上下文(ctx)和一个取消函数(cancel),然后使用一个select让主程序陷入等待:等待1秒后打印overslept退出或者等待ctx过期后退出。 因为ctx50秒后就过期,所以ctx.Done()会先接收到值,上面的代码会打印ctx.Err()取消原因。
3、 WithTimeout
-
创建一个可以指定超时时间timeout的该传入context对象的派生context对象(底层调用的还是WithDeadline(),也就是相当于调用了
WithDeadline(parent, time.Now().Add(timeout))
),以及一个用于手动cancel该派生context的回调函数cancelFunc()
-
当过了超时时间timeout时,或者当调用返回的cancel函数时,或者当parent Context(的Done通道)关闭时,返回的派生Context对象(的Done通道)将被关闭,同时往该 Done通道内存入一个值。此时如果能从该Context的Done通道内取出一值即为表示任务被cancel或者执行超时了,应立即释放与该context有关的资源,并关闭该goroutine的运行,释放其所占的资源
函数签名:
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
通常用于数据库或者网络连接的超时控制。具体示例如下:
package main
import (
"context"
"fmt"
"sync"
"time"
)
// context.WithTimeout
var wg sync.WaitGroup
func worker(ctx context.Context) {
LOOP:
for {
fmt.Println("db connecting ...")
time.Sleep(time.Millisecond * 10) // 假设正常连接数据库耗时10毫秒
select {
case <-ctx.Done(): // 50毫秒后自动调用
break LOOP
default:
}
}
fmt.Println("worker done!")
wg.Done()
}
func main() {
// 设置一个50毫秒的超时
ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*50)
wg.Add(1)
go worker(ctx)
time.Sleep(time.Second * 5)
cancel() // 通知子goroutine结束
wg.Wait()
fmt.Println("over")
}
4、 WithValue()
-
WithValue()函数能够将请求作用域的数据与 Context 对象建立关系:创建一个持有key-val 这个键值对的传入的 parent context对象的派生context对象
。 -
只应在API和进程间传递请求域的数据时使用Context值,而不是使用它来传递可选参数给函数。
-
所提供的键必须是可比较的,并且不应该是string类型或任何其他内置类型,以避免使用上下文在包之间发生冲突。WithValue的用户应该为键定义自己的类型。为了避免在分配给interface{}时进行分配,上下文键通常具有具体类型struct{}。或者,导出的上下文关键变量的静态类型应该是指针或接口。
函数声明:
func WithValue(parent Context, key, val interface{}) Context
示例:
package main
import (
"context"
"fmt"
"sync"
"time"
)
// context.WithValue
type TraceCode string
var wg sync.WaitGroup
func worker(ctx context.Context) {
key := TraceCode("TRACE_CODE")
traceCode, ok := ctx.Value(key).(string) // 在子goroutine中获取trace code
if !ok {
fmt.Println("invalid trace code")
}
LOOP:
for {
fmt.Printf("worker, trace code:%s\n", traceCode)
time.Sleep(time.Millisecond * 10) // 假设正常连接数据库耗时10毫秒
select {
case <-ctx.Done(): // 50毫秒后自动调用
break LOOP
default:
}
}
fmt.Println("worker done!")
wg.Done()
}
func main() {
// 设置一个50毫秒的超时
ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*50)
// 在系统的入口中设置trace code传递给后续启动的goroutine实现日志数据聚合
ctx = context.WithValue(ctx, TraceCode("TRACE_CODE"), "12512312234")
wg.Add(1)
go worker(ctx)
time.Sleep(time.Second * 5)
cancel() // 通知子goroutine结束
wg.Wait()
fmt.Println("over")
}