使用方法
context
用于表示一个请求的上下文。一个网络请求,一般开启一个协程处理,而这个协程内部还会开启其它的协程继续处理。为了传递一个请求在不同协程中的处理情况(比如是否超时等),我们利用context
来记录这个情况。同样的,对于一些IO处理,也需要context
保存状态,比如超时时间等。
context
一般用以下两种方式传递:
- 作为函数调用的第一个参数
- 作为一个请求结构体的可选配置
func (d *Dialer) DialContext(ctx context.Context, network, address string) (Conn, error){}
func (r *Request) WithContext(ctx context.Context) *Request
context
应该在程序中流动,而不应该存储,不要直接存储context
在结构体中等。一般来说,一个请求的context
在main
中使用context.BackGorund()
方法生成。一般来说,这个用于控制超时时间。
context
可以继承,一旦一个context
取消了,那么继承自该context
的所有context
都会取消,但是不会影响它的父context
。
context
可以携带一些值,比如:
func WithValue(parent Context, key, val interface{}) Contex
key
必须是可比较的,而且不要是内嵌类型,自定义类型,比如:
一般来说,context
携带的值应该是request-scoped
类型的,即仅仅是本次请求范围内会用到的。像数据库字段之类的,不要在这里写入,因为这是全局都用的。使用context
方式传递数据,会造成函数参数意义不明的情况,给出对比:
// 参数意义不明确
func IsAdminUser(ctx context.Context) bool {
x := token.GetToken(ctx)
userObject := auth.AuthenticateToken(x)
return userObject.IsAdmin() || userObject.IsRoot()
}
// 明确参数的意义,这是合理的方式
func IsAdminUser(token string, authService AuthService) int {
userObject := authService.AuthenticateToken(token)
return userObject.IsAdmin() || userObject.IsRoot()
}
但是,有些情况下,可以简化API的设计。比如我们有一个较长的函数调用链,向userId
这种数据,却要从头到尾传递整个调用链,这不是一个好的方法。我们此时可以把这些数据封装到context
中。这些数据不能是控制类型的字段,即传入函数的执行结果,最好不要依赖这些值;但是log
等函数,可以打印这些数据,因为这部影响函数执行结果。
一般不建议使用context.Value
的模式,这在后期重构等时候,造成一些不好的影响,除非必要,否则勿用。
- 不要把Context放在结构体中,要以参数的方式传递,parent Context一般为Background
- 应该要把Context作为第一个参数传递给入口请求和出口请求链路上的每一个函数,放在第一位,变量名建议都统一,如ctx。
- 给一个函数方法传递Context的时候,不要传递nil,否则在tarce追踪的时候,就会断了连接
- Context的Value相关方法应该传递必须的数据,不要什么数据都使用这个传递
- Context是线程安全的,可以放心的在多个goroutine中传递
- 可以把一个 Context 对象传递给任意个数的 gorotuine,对它执行取消操作时,所有 goroutine 都会接收到取消信号。
参考资料:
- https://blog.golang.org/context
- https://juejin.im/post/5a6873fef265da3e317e55b6
- https://www.ardanlabs.com/blog/2019/09/context-package-semantics-in-go.html
- http://p.agnihotry.com/post/understanding_the_context_package_in_golang/
- https://medium.com/@cep21/how-to-correctly-use-context-context-in-go-1-7-8f2c0fafdf39