内容简介:由于主观上我也不是很喜欢根据
context
的设计在Golang中算是一个比较有争议的话题。 context
不是银弹,它解决了一些问题的同时,也有不少让人诟病的缺点。本文主要探讨一下 context
的优缺点以及一些使用建议。
缺点
由于主观上我也不是很喜欢 context
的设计,所以我们就从缺点先开始吧。
到处都是 context
根据 context
使用的官方建议, context
应当出现在函数的第一个参数上。这就直接导致了代码中到处都是 context
。作为函数的调用者,即使你不打算使用 context
的功能,你也必须传一个占位符—— context.Background()
或 context.TODO()
。这无疑是一种code smell,特别是对于有代码洁癖 程序员 来说,传递这么多无意义的参数是简直是令人无法接受的。
Err()
其实很鸡肋
context.Context
接口中有定义 Err()
方法:
type Context interface { ... // If Done is not yet closed, Err returns nil. // If Done is closed, Err returns a non-nil error explaining why: // Canceled if the context was canceled // or DeadlineExceeded if the context's deadline passed. // After Err returns a non-nil error, successive calls to Err return the same error. Err() error ... } 复制代码
当触发取消的时候(这通常意味着发生了一些错误或异常),可以通过 Err()
方法来查看错误的原因。这的确是一个常见的需求,但 context
包里面对 Err()
的实现却显得有点鸡肋, Err()
反馈的错误信息仅限于如下两种:
- 因取消而取消 (excuse me???)
- 因超时而取消
// Canceled is the error returned by Context.Err when the context is canceled. var Canceled = errors.New("context canceled") // DeadlineExceeded is the error returned by Context.Err when the context's // deadline passes. var DeadlineExceeded error = deadlineExceededError{} type deadlineExceededError struct{} func (deadlineExceededError) Error() string { return "context deadline exceeded" } func (deadlineExceededError) Timeout() bool { return true } func (deadlineExceededError) Temporary() bool { return true } 复制代码
从 Err()
方法中你几乎不能得到任何与业务相关的错误信息,也就是说,如果你想知道具体的取消原因,你不能指望 context
包,你得自己动手丰衣足食。如果 cancel()
方法能接收一个错误可能会好一些:
ctx := context.Background() c, cancel := context.WithCancel(ctx) err := errors.New("some error") cancel(err) //cancel的时候能带上错误原因 复制代码
context.Value
——没有约束的自由是危险的
context.Value几乎就是一个 map[interface{}]interface{}
:
type Context interface { ... Value(key interface{}) interface{} ... } 复制代码
这给了程序员们极大的自由,几乎就是想放什么放什么。但这种几乎毫无约束的自由是很危险的,不仅容易引起滥用,误用,而且失去了编译时的类型检查,要求我们对 context.Value
中的每一个值都要做类型断言,以防 panic
。尽管文档中说明了 context.Value
中应当用于保存“request-scoped”类型的数据,可对于什么是“request-scoped”,一千个人的眼中有一千种定义。像 request-id,access_token,user_id
这些数据,可以当做是“request-scoped”放在 context.Value
里,也完全可以以更清晰的定义方式定义在结构体里。
可读性很差
可读性差也是自由带来的代价,在学习阅读 Go 代码的时候,看到 context
是令人头疼的一件事。如果文档注释的不够清晰,你几乎无法得知 context.Value
里究竟包含什么内容,更不谈如何正确的使用这些内容了。下面的代码是 http.Request
结构体中 context
的定义和注释:
// http.Request type Request struct { .... // ctx is either the client or server context. It should only // be modified via copying the whole Request using WithContext. // It is unexported to prevent people from using Context wrong // and mutating the contexts held by callers of the same request. ctx context.Context } 复制代码
请问你能看出来这个 context.Value
里面会保存什么吗?
... func main () { http.Handle("/", http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { fmt.Println(req.Context()) // 猜猜看这个context里面有什么? })) } 复制代码
写到这里我不禁想起来了“奶糖哥”的灵魂拷问:桌上这几杯酒,哪一杯是茅台?
即使你将 context
打印了出来,你也无法得知 context
跟函数入参之间的关系,说不定下次传另一组参数, context
里面的值就变了呢。通常遇到这种情况,如果文档不清晰(很遗憾的是我发现大部分代码都不会对 context.Value
有清晰的注释),只能全局搜索 context.WithValue
,一行行找了。
优点
虽然主观上我对 context
是有一定“偏见”的,但客观上,它还是具备一些优点和功劳的。
统一了cancelation的实现方法
许多文章都说 context
解决了
goroutine的cancelation问题,但实际上,我觉得cancelation的实现本身不算是一个问题,利用关闭 channel
的广播特性,实现cancelation是一件比较简单的事情,举个栗子:
// Cancel触发一个取消 func Cancel(c chan struct{}) { select { case <-c: //已经取消过了, 防止重复close default: close(c) } } // DoSomething做一些耗时操作,可以被cancel取消。 func DoSomething(cancel chan struct{}, arg Arg) { rs := make(chan Result) go func() { // do something rs <- xxx //返回处理结果 }() select { case <-cancel: log.Println("取消了") case result := <-rs: log.Println("处理完成") } } 复制代码
或者你也可以把用于取消的 channel
放到结构体里:
type Task struct{ Arg Arg cancel chan struct{} //取消channel } // NewTask 根据参数新建一个Task func NewTask(arg Arg) *Task{ return &Task{ Arg:arg , cancel:make(chan struct{}), } } // Cancel触发一个取消 func (t *Task) Cancel() { select { case <-t.c: //已经取消过了, 防止重复close default: close(t.c) } } // DoSomething做一些耗时操作,可以被cancel取消。 func (t *Task) DoSomething() { rs := make(chan Result) go func() { // do something rs <- xxx }() select { case <-t.cancel: log.Println("取消了") case result := <-rs: log.Println("处理完成") } } // t := NewTask(arg) // t.DoSomething() 复制代码
可见,对cancelation的实现也是多种多样的。一千个程序员由可能写出一千种实现方式。不过幸亏有 context
统一了cancelation的实现,不然怕是每引用一个库,你都得额外学习一下它的cancelation机制了。我认为这是 context
最大的优点,也是最大的功劳。gopher们只要看到函数中有 context
,就知道如何取消该函数的执行。如果想要实现cancelation,就会优先考虑 context
。
提供了一种不那么优雅,但是有效的传值方式
context.Value
是一把双刃剑,上文中提到了它的缺点,但只要运用得当,缺点也可以变优点。 map[interface{}]interface{}
的属性决定了它几乎能存任何内容,如果某方法需要cancelation的同时,还需要能接收调用方传递的任何数据,那 context.Value
还是十分有效的方式。如何“运用得当”请参考下面的使用建议。
context
使用建议
需要cancelation的时候才考虑 context
context
主要就是两大功能,cancelation和 context.Value
。如果你仅仅是需要在goroutine之间传值,请不要使用 context
。因为在Go的世界里, context
一般默认都是能取消的,一个不能取消的 context
很容易被调用方误解。
一个不能取消的 context
是没有灵魂的。
context.Value
能不用就不用
context.Value
内容的存取应当由库的使用者来负责
。如果是库内部自身的数据流转,那么请不要使用 context.Value
,因为这部分数据通常是固定的,可控的。假设某系统中的鉴权模块,需要一个字符串 token
来鉴权,对比下面两种实现方式,显然是显示将 token
作为参数传递更清晰。
// 用context func IsAdminUser(ctx context.Context) bool { x := token.GetToken(ctx) userObject := auth.AuthenticateToken(x) return userObject.IsAdmin() || userObject.IsRoot() } // 不用context func IsAdminUser(token string, authService AuthService) int { userObject := authService.AuthenticateToken(token) return userObject.IsAdmin() || userObject.IsRoot() } 复制代码
示例代码来源: How to correctly use context.Context in Go 1.7
所以,请忘了“request-scoped”吧,把 context.Value
想象成是“user-scoped”——让用户,也就是库的调用者来决定在 context.Value
里面放什么。
使用 NewContext
和 FromContext
对来存取 context
不要直接使用 context.WithValue()
和 context.Value("key")
来存取数据,将 context.Value
的存取做一层封装能有效降低代码冗余,增强代码可读性同时最大限度的防止一些粗心的错误。 context.Context
接口中注释为我们提供了一个很好的示例:
package user import "context" // User is the type of value stored in the Contexts. type User struct {...} // key is an unexported type for keys defined in this package. // This prevents collisions with keys defined in other packages. type key int // userKey is the key for user.User values in Contexts. It is // unexported; clients use user.NewContext and user.FromContext // instead of using this key directly. var userKey key // NewContext returns a new Context that carries value u. func NewContext(ctx context.Context, u *User) context.Context { return context.WithValue(ctx, userKey, u) } // FromContext returns the User value stored in ctx, if any. func FromContext(ctx context.Context) (*User, bool) { u, ok := ctx.Value(userKey).(*User) return u, ok } 复制代码
如果使用 context.Value
,请注释清楚
上面提到, context.Value
可读性是十分差的,所以我们不得不用文档和注释的方式来进行弥补。至少列举所有可能的 context.Value
以及它们的get set方法( NewContext(),FromContext()
),尽可能的列举函数入参与 context.Value
之间的关系,给阅读或维护你代码的人多一份关爱。
封装以减少 context.TODO()
或 context.Background()
对于那些提供了 context
的方法,但作为调用方我们并不使用的,还是不得不传 context.TODO()
或 context.Background()
。如果你不能忍受大量无用的 context
在代码中扩散,可以对这些方法做一层封装:
// 假设有如下查询方法,但我们几乎不使用其提供的context func QueryContext(ctx context.Context, query string, args []NamedValue) (Rows, error) { ... } // 封装一下 func Query(query string, args []NamedValue) (Rows, error) { return QueryContext(context.Background(), query, args) } 复制代码
以上所述就是小编给大家介绍的《Golang Context的好与坏及使用建议》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- 为什么建议你使用枚举?
- 使用 Elasticsearch 的 44 条建议
- 一些 PyCharm 的使用和设置建议
- FairyGUI的使用技巧和优化建议
- 关于Redis的一些新特性 ,使用建议和最佳实践
- 为什么纯php文件不建议使用结束标签
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
你必须知道的495个C语言问题
Steve Summit / 孙云、朱群英 / 人民邮电出版社 / 2009-2 / 45.00元
“本书是Summit以及C FAQ在线列表的许多参与者多年心血的结晶,是C语言界最为珍贵的财富之一。我向所有C语言程序员推荐本书。” ——Francis Glassborow,著名C/C++专家,ACCU(C/C++用户协会)前主席 “本书清晰阐明了Kernighan与Ritchie《The C programming Language》一书中许多简略的地方,而且精彩地总结了C语言编程......一起来看看 《你必须知道的495个C语言问题》 这本书的介绍吧!