概述
相信大家在日常使用Go语言进行开发的时候一定会遇到下面这种代码,特别是在Web开发中使用了诸如Gin、Echo和Beego类似的开发框架。
func getUser(ctx context.Context, id int64){
getUserById(ctx, id)
}
func getUserById(ctx context.Context, id int64){
...
}
能够从上面的代码中看出,每个方法的第一个参数都是Context
。
Context
是Go语言在1.7版本引入的,Context
可以用来在goroutine
之间传递上下文信息,通常可以用来进行超时控制,消息传递等,Go语言官方建议的将Context
作为函数的第一个参数并不断地透传下去以实现在不同的goroutine
之间传递上下文。
Context的使用
context
包中提供了两种方式创建Context
:
context.Backgroud()
context.TODO()
通过这两种方式都可以创建一个Context
,而且两者之间没有区别,官方给出的定义是:
context.Background
是上下文的默认值,所有其他的上下文都应该从它衍生(Derived)出来;context.TODO
应该只在不确定应该使用哪种上下文时使用;
使用这两种方法创建出来的Context
都是默认的Context
,也可以称为父Context
,不具备我们上面说到的消息传递,超时控制这些功能,需要实现这些功能就需要使用context
包提供的另外几个创建Context
的方法:
context.WithCancel(parent Context)
context.WithDeadline(parent Context, deadline time.Time)
context.WithTimeout(parent Context, timeout time.Duration)
context.WithValue(parent Context, key, val interface{})
传递数据
在日常的开发中,日志是必不可少的一部分,同时我们都希望在日志打印的时候能够携带一个tranceID
,这样的话根据一个tranceID
就可以关联出一个请求的所有日志,这个在Go语言中就可以使用Context
的传递数据方式实现。
const Key = "trance_id"
func main() {
ctx := context.WithValue(context.Background(), Key, "1001")
router(ctx)
}
func router(ctx context.Context) {
printLog(ctx, "router方法的日志")
service(ctx)
}
func service(ctx context.Context) {
printLog(ctx, "service方法的日志")
}
func printLog(ctx context.Context, msg string) {
fmt.Printf("%s | level=info | trance_id=%s | message=%s\n", time.Now().Format("2006-01-02 15:04:05"), ctx.Value(Key), msg)
}
上面的代码中通过WithValue
方法创建了一个可以携带数据的Context
,然后在方法调用链中将Context
作为第一个参数一直传递,在每个方法中都可以通过Value
取到最开始放进去的tranceID
,最后打印结果如下:
2024-07-18 19:44:24 | level=info | trance_id=1001 | message=router方法的日志
2024-07-18 19:44:24 | level=info | trance_id=1001 | message=service方法的日志
超时控制
要使用Context
实现超时控制可以使用WithTimeout
或WithDeadline
,通过这两个方法创建出来的Context
都可以实现超时控制,两个方法的作用是一样的。同时两个方法都会返回一个cancel
方法,通过调用这个方法可以提前取消,不需要等到传入的时间达到即可取消。
示例代码如下:
func main() {
ctx, cancelFunc := context.WithTimeout(context.Background(), 3*time.Second)
defer cancelFunc()
test(ctx)
}
func test(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("test = %d\n", i)
}
}
}
上面的代码中通过WithTimeout
创建了一个时间为3
秒的Context
,然后在test
方法中进行循环,每隔1
秒输出一个test
语句,但是由于Context
的超时设置的3
秒,所以当3
秒后Context
就会自动取消,最后test
的输出也就只有2行。
test = 0
test = 1
context deadline exceeded
WithCancel取消控制
在开发中有时候可能需要同时创建多个goroutine
并行的去进行一些逻辑的处理,但是当主goroutine
出现错误的时候,这时候我们就已经不需要其他的goroutine
继续执行,这种情况我们就可以使用WithCancel
创建一个可以取消的Context
,将Context
传入到不同的goroutine
中,当主goroutine
出错时,直接调用cancel
方法就可以取消所有的goroutine
。
示例代码如下:
func main() {
ctx, cancelFunc := context.WithCancel(context.Background())
go test(ctx)
time.Sleep(3 * time.Second)
cancelFunc()
time.Sleep(time.Second)
}
func test(ctx context.Context) {
for i := 0; i < 10; i++ {
time.Sleep(1 * time.Second)
select {
case <-ctx.Done():
fmt.Println("取消执行")
return
default:
fmt.Printf("test = %d\n", i)
}
}
}
上面的代码通过WithCancel
创建了一个可以取消的Context
,然后创建了一个goroutine
执行test
方法,当主goroutine
等待3秒后调用cancel
方法取消,这时候执行test
方法的goroutine
就会直接退出不再执行。
test = 0
test = 1
取消执行