在Go语言中,我们通常使用context包来帮助我们传递请求的状态。context包提供了一个Context接口类型、一个Background函数和一个WithCancel、WithDeadline、WithTimeout和WithValue函数。
什么是context
Context是Go语言中的一个标准库,它被用于携带请求的deadline、取消信号、请求级别的数据以及请求的Trace信息等。我们可以通过context包来构建一个树状结构的Context,它用于在不同的服务或者模块之间,以及不同的goroutine之间传递上下文信息。
context的使用场景
context包一般适合用于多个goroutine之间的共享数据,或者一些更加抽象的方法,也就是说,当我们需要在某一个请求或者事件中携带发起这个请求或者事件环境信息时,就可以使用context。具体来说,它有如下几个应用场景:
请求需要携带截止时间
请求需要携带上下文环境信息
需要在多个Goroutine之间传递上下文信息
需要控制Goroutine的生命周期
context实现请求日志记录
在Go语言中,我们可以通过context包来记录请求的日志信息。我们需要在处理请求的一些方法中创建一个带有context对象的logger,当请求被处理时,我们可以使用context对象将logger传递给所有涉及到的Goroutine,并在每个Goroutine中使用这个logger进行请求日志的记录。
使用context创建请求上下文
我们可以通过"context.Background()"来创建一个空的context,然后使用"context.WithValue(parent context.Context, key, val interface{})"方法将请求的上下文信息存储在context对象中:
func handleRequest(w http.ResponseWriter, r *http.Request) {
// 创建一个空的Context
ctx := context.Background()
// 向Context中添加请求的Trace信息
ctx = context.WithValue(ctx, "trace_id", "123456")
// 向Context中添加请求发起时间信息
ctx = context.WithValue(ctx, "start_time", time.Now())
// 处理请求
processRequest(ctx, w, r)
}
在这个例子中,我们创建了一个空的Context对象,并向其中添加了两条请求上下文信息:trace_id和start_time。在处理完请求之后,我们需要到processRequest函数中去做真正的日志记录工作。
在Goroutine及函数中使用context
在处理请求的方法中,我们需要使用"context.WithValue(parent context.Context, key, val interface{})"方法来添加请求的上下文信息。在使用这个方法时,我们首先需要创建一个空的Context对象。然后再通过WithCancel、WithDeadline、WithTimeout等函数,创建一个新的Context对象,这个新的Context对象会继承父Context对象的键值对,并拥有自己的Cancel或者Deadline。这样,当子Context对象被cancel或者deadline超时时,父Context对象也会跟着cancel或timeout。
例如,我们可以在处理请求的方法中开启一个新的Goroutine,这个Goroutine将会从父Context对象中继承所有的键值对,并可以在处理请求的方法退出时停止执行。可以在这个Goroutine中使用这些键值对进行日志的记录。
func processRequest(ctx context.Context, w http.ResponseWriter, r *http.Request) {
// 使用Context从请求中获取trace_id
traceId := ctx.Value("trace_id").(string)
// 使用Context从请求中获取start_time
startTime := ctx.Value("start_time").(time.Time)
// 为每个请求的handler生成一个专属logger对象
logger := log.New(os.Stdout, "", log.LstdFlags)
// 使用goroutine记录请求的处理日志
go func() {
// 调用其他服务接口时,需要将父Context传递给子Context
newCtx := context.WithValue(ctx, "step", "calling service a")
callServiceA(newCtx, logger)
newCtx = context.WithValue(ctx, "step", "calling service b")
callServiceB(newCtx, logger)
logger.Printf("finised processing request in %v\n", time.Now().Sub(startTime))
}()
// 处理请求
w.Write([]byte(fmt.Sprintf("hello, trace_id=%s", traceId)))
}
在这个例子中,我们在处理请求的方法中开启了一个新的goroutine,用于并发执行耗时操作。我们使用"context.WithValue(parent context.Context, key, val interface{})"方法创建了一个新的Context对象,并传递给了耗时操作的函数。在处理请求的方法结束时,我们使用logger对象打印出请求处理的时间。
调用其他服务接口时,如何传递context
当我们调用其他服务接口时,我们需要将父Context对象传递给子Context对象,以确保在子服务中可以继续使用父Context中的请求上下文信息。
例如,我们可以在调用其他服务接口的方法中使用"context.WithValue(parent context.Context, key, val interface{})"方法,并将父Context对象作为第一个参数传递进来,这样,在调用其他服务接口时,我们就可以使用这个包含请求上下文信息的Context。
func callServiceA(ctx context.Context, logger *log.Logger) {
// 调用service A
// ...
// 记录日志
step := ctx.Value("step").(string)
logger.Printf("%s took %f seconds\n", step, time.Now().Sub(startTime).Seconds())
}
在这个例子中,我们使用"context.WithValue(parent context.Context, key, val interface{})"方法将父Context对象中的请求上下文信息,添加到了子Context中,然后使用子Context对象调用了service A。
总结
在这篇文章中,我们介绍了Go语言中如何使用context来实现请求日志记录。我们首先了解了context的概念和使用场景,然后详细介绍了如何在处理请求的方法中使用context对象来记录请求的上下文信息,以及如何在Goroutine和子函数中使用context对象。