1. 前言
在Go语言的开发领域中,使用context是非常普遍的。它可以有效地传递值和取消操作等,为我们的应用程序创建一个可控的环境。然而,如果使用不当,会导致减慢程序的性能。因此,本文将为大家提供优化Go中使用context的一些方法。
2. 什么是context?
Context是Go在1.7版本中引入的一个标准库。它是Go语言中用于描述一个请求的环境的一种方法。可以通过context.Context类型来实现它。它提供了用于跨函数或者协程传递请求范围的值、取消信号和超时信号等等。
2.1 context.Context类型
context.Context是一个接口类型,它主要包括了以下三个方法:
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key interface{}) interface{}
}
- Deadline:返回该Context被取消的时间,第一个返回值(deadline)表示可以等待的最长时间,第二个返回值(ok)表示是否有截止时间
- Done:返回一个只读的通道(类型为<-chan struct{}),在上下文结束时,该通道会被关闭。
- Err:返回取消原因,也就是Context被取消的原因,也就是context.WithCancel、context.WithTimeout等函数取消Context时传递的错误值。
- Value:获取传递给Context的键值对。
2.2 context.WithValue
通过context.WithValue函数,我们可以将值(键值对)存储在Context中。这个值可以被复制并传递给程序的各个部分,但是,当您在使用Context时,可以在Context结束时同时清理它们。
type key int
const (
userKey key = iota
authTokenKey
)
func serve(ctx context.Context) {
db := sql.Open("mysql", "dataSource")
user := db.User(ctx.Value(userKey).(int))
authToken := ctx.Value(authTokenKey).(string)
// ...
}
func main() {
ctx := context.WithValue(context.Background(), userKey, 42)
ctx = context.WithValue(ctx, authTokenKey, "some_token")
serve(ctx)
}
使用上下文来传递值非常常见,但是它会对性能产生负面影响。
3. Go中使用context的常见误区
3.1 不需要Context的函数带上Context参数
这是一个常见的错误,为了保持一致性,在代码的各个部分中都加上了一个context参数,即使它们在该函数中没有任何作用。
例如,下面的代码,GetUser函数有一个参数类型为context.Context,当然,在该函数中并未使用它。
func getUser(ctx context.Context, userID int) (*User, error) {
db, err := sql.Connect(...)
if err != nil {
return nil, err
}
return db.GetUser(userID)
}
当代码增长时,每个函数都会保留一个context参数,这会使代码异常冗长。此外,每个创建的context都要进行相应的垃圾回收。
3.2 在函数之间随意传递Context
为了方便,在函数之间传递Context,每个函数都可以直接将参数中的Context传递给下一个函数。这是一种显式方法,它增加了代码中的实用性和可读性。但是,通常情况下,这种方法也会带来不必要的性能问题。
这个因为一个Context是一个链式的环境。如果我们仅仅想从中获取一个已经存在的键值对,通过传递整个Context链来查询它显然是很浪费的。这样会导致context传递的负载过大,提高了Go代码运行的时间。
4. 优化Context
4.1 避免不需要的Context
在函数参数中添加不必要的Context对代码性能有很大的影响。因此,最佳实践是在真正需要Context的地方使用它。
4.2 传递必要的参数
当我们需要Context时,我们可以考虑从Context中直接获取必要的参数并将其传递给下一个函数。这样一来,在函数之间传递Context时,必要的参数只会传递一次,而不会每个函数都传递一遍。使用这种方法会避免不必要的性能问题。
4.3 定时清除
当使用context.WithValue函数向Context中加入键值对时,我们需要注意一点:确保在Context结束时立即清除键值对。
当我们取消操作或者超时时,Context结束。如果我们忘记了清除键值对,它们将保留在Context中,并可能导致内存泄漏。
db, err := sql.Connect(...)
if err != nil {
return nil, err
}
ctx, cancel := context.WithTimeout(context.Background(), 5 * time.Second)
defer cancel()
ctx = context.WithValue(ctx, dbKey{}, db)
serve(ctx)
上面的代码中,我们忘记在Context被取消时清除键值对。我们可以通过defer语句解决这个问题。
4.4 分离取消和超时
当您创建Context时,您有两种方式可以在某个时间点结束Context。其中一种方式是在某个时间点结束。另一种方式是在持续一段时间后结束。
如果您的应用程序同时使用cancel和timeout对Context进行终止,那么它会变得非常混乱。因此,最佳做法是将取消和超时分开处理。我们可以在程序中设置一个上限,如果在这个上限之前程序没有完成,我们可以将它标记为错误并非取消。
ctx, cancel := context.WithTimeout(ctx, 30 * time.Second)
defer cancel()
result, err := funcRunWithDeadline(ctx)
if context.DeadlineExceeded == err {
return nil, errors.New("deadline exceeded")
}
if err != nil {
return nil, err
}
4.5 使用context.Value慎重
在很多情况下,我们宁愿传递具体的参数,而不是使用context.Value。对于那些在整个程序中都具有普适性的值,使用context.Value是非常好用的。例如,跨请求传递某些信息,用户的身份验证信息等等。
5. 总结
通过上述分析,我们发现,在Go语言中使用context是不可避免的,这是一个非常重要且有用的工具。同时,我们也要意识到,在使用Context时要避免一些常见误区,如不需要Context的函数带上Context参数和在函数之间随意传递Context。
为了优化Context的性能,我们可以使用一些技巧,如避免不必要的Context,传递必要的参数,定时清除Context,分离取消和超时,以及慎重使用context.Value。
如果我们可以在代码中遵循这些最佳实践,那么我们就可以大大提高Go语言程序的性能。