context
包主要提供了两种方式创建context:
context.Background()
context.TODO()
这两个函数骑士只是互为别名,没有差别,官方给的定义是:
context.Background
是上下文的默认值,所有其他的上下文都应该从它衍生出来。context.TODO
应该只在不确定应该使用哪种上下文时使用;所以在大多数情况下,我们都使用context.Background
作为起始的上下文向下传递。
上面的两种方式是创建根context
,不具备任何功能,具体实践还是要依靠context
包提供的With
系列函数来进行派生:
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
func WithValue(parent Context, key, val interface{}) Context
这四个函数都要基于Context
衍生,通过这些函数,就创建了一棵Context树,树的每个节点都可以有任意多个子节点,节点层级可以有任意多个,如下图表示:
基于一个父Context
可以随意衍生,其实这就是一个Context
树,树的每个节点都可以有任意多个子节点,节点层级可以有任意多个,每个子节点都依赖于其父节点,例如上图,我们可以基于Context.Background
衍生出四个子context:ctx1.0-cancel
、ctx2.0-deadline
、ctx3.0-timeout
、ctx4.0-withvalue
,这四个子context
还可以作为父context
继续向下衍生,即使其中ctx1.0-cancel
节点取消了,也不影响其他三个父节点分支。
创建context
方法和context
的衍生方法就这些,下面我们就一个一个来看一下他们如何被使用。
我们日常在业务开发中都希望能有一个trace_id
能串联所有的日志,这就需要我们打印日志时能够获取到这个trace_id
,在Go
语言中我们就可以使用Context
来传递,通过使用WithValue
来创建一个携带trace_id
的context
,然后不断透传下去,打印日志时输出即可,来看使用例子:
package main
import (
"context"
"fmt"
"github.com/google/uuid"
"strings"
"time"
)
func NewTraceID() string {
return strings.Replace(uuid.New().String(), "-", "", -1)
}
func NewContextWithValue() context.Context {
return context.WithValue(context.Background(), "trace_id", NewTraceID())
}
func GetContextValue(ctx context.Context, key string) string {
value, ok := ctx.Value(key).(string)
if !ok {
return ""
}
return value
}
func LogRecord(ctx context.Context, message string) {
fmt.Printf("%s|info|trace_id=%s|%s\n", time.Now().Format("2006-01-02 15:04:05"), GetContextValue(ctx, "trace_id"), message)
}
func ProcessEnter(ctx context.Context) {
LogRecord(ctx, "hello world")
}
func main() {
ProcessEnter(NewContextWithValue())
}
输出结果:
2024-05-06 21:57:03|info|trace_id=c8adea6447534ecfbd69dd37a39ba8ce|hello world
我们基于context.Background
创建一个携带trace_id
的ctx
,然后通过context
树一起传递,从中派生的任何context
都会获取此值,我们最后打印日志的时候就可以从ctx
中取值输出到日志中。目前一些RPC
框架都是支持了Context
,所以trace_id
的向下传递就更方便了。
通常健壮的程序都是要设置超时时间的,避免因为服务端长时间响应消耗资源,所以一些web
框架或rpc
框架都会采用withTimeout
或者withDeadline
来做超时控制,当一次请求到达我们设置的超时时间,就会及时取消,不在往下执行。withTimeout
和withDeadline
作用是一样的,就是传递的时间参数不同而已,他们都会通过传入的时间来自动取消Context
,这里要注意的是他们都会返回一个cancelFunc
方法,通过调用这个方法可以达到提前进行取消,不过在使用的过程还是建议在自动取消后也调用cancelFunc
去停止定时减少不必要的资源浪费。
withTimeout
、WithDeadline
不同在于WithTimeout
将持续时间作为参数输入而不是时间对象,这两个方法使用哪个都是一样的,看业务场景和个人习惯了,因为本质withTimout
内部也是调用的WithDeadline
。
现在我们就举个例子来试用一下超时控制,现在我们就模拟一个请求写两个例子:
func main() {
HttpHandler()
}
func NewContextWithTimeout() (context.Context,context.CancelFunc) {
return context.WithTimeout(context.Background(), 3 * time.Second)
}
func HttpHandler() {
ctx, cancel := NewContextWithTimeout()
defer cancel()
deal(ctx)
}
func deal(ctx context.Context) {
for i:=0; i< 10; i++ {
time.Sleep(1*time.Second)
select {
case <- ctx.Done():
fmt.Println(ctx.Err())
return
default:
fmt.Printf("deal time is %d\n", i)
}
}
}
输出结果:
deal time is 0
deal time is 1
deal time is 2
context deadline exceeded
func main() {
HttpHandler1()
}
func NewContextWithTimeout1() (context.Context,context.CancelFunc) {
return context.WithTimeout(context.Background(), 3 * time.Second)
}
func HttpHandler1() {
ctx, cancel := NewContextWithTimeout1()
defer cancel()
deal1(ctx, cancel)
}
func deal1(ctx context.Context, cancel context.CancelFunc) {
for i:=0; i< 10; i++ {
time.Sleep(1*time.Second)
select {
case <- ctx.Done():
fmt.Println(ctx.Err())
return
default:
fmt.Printf("deal time is %d\n", i)
cancel()
}
}
}
输出结果:
deal time is 0
context canceled
使用起来还是比较容易的,既可以超时自动取消,又可以手动控制取消。这里大家要记的一个坑,就是我们往从请求入口透传的调用链路中的context是携带超时时间的,如果我们想在其中单独开一个goroutine去处理其他的事情并且不会随着请求结束后而被取消的话,那么传递的context要基于context.Background或者context.TODO重新衍生一个传递,否决就会和预期不符合了。
日常业务开发中我们往往为了完成一个复杂的需求会开多个gouroutine去做一些事情,这就导致我们会在一次请求中开了多个goroutine确无法控制他们,这时我们就可以使用withCancel来衍生一个context传递到不同的goroutine中,当我想让这些goroutine停止运行,就可以调用cancel来进行取消。
来看一个例子:
func main() {
ctx,cancel := context.WithCancel(context.Background())
go Speak(ctx)
time.Sleep(10*time.Second)
cancel()
time.Sleep(1*time.Second)
}
func Speak(ctx context.Context) {
for range time.Tick(time.Second){
select {
case <- ctx.Done():
fmt.Println("我要闭嘴了")
return
default:
fmt.Println("balabalabalabala")
}
}
}
运行结果:
balabalabalabala
....省略
balabalabalabala
我要闭嘴了
我们使用withCancel
创建一个基于Background
的ctx
,然后启动一个讲话程序,每隔1s说一话,main
函数在10s后执行cancel
,那么speak
检测到取消信号就会退出。
基于Nginx+Supervisord+uWSGI+Django1.11.1+Python3.6.5构建