context
的設計在Golang中算是一個比較有爭議的話題。context
不是銀彈,它解決了一些問題的同時,也有很多讓人詬病的缺點。本文主要探討一下context
的優缺點以及一些使用建議。git
因爲主觀上我也不是很喜歡context
的設計,因此咱們就從缺點先開始吧。程序員
context
根據context
使用的官方建議,context
應當出如今函數的第一個參數上。這就直接致使了代碼中處處都是context
。做爲函數的調用者,即便你不打算使用context
的功能,你也必須傳一個佔位符——context.Background()
或context.TODO()
。這無疑是一種code smell,特別是對於有代碼潔癖程序員來講,傳遞這麼多無心義的參數是簡直是使人沒法接受的。github
Err()
其實很雞肋context.Context
接口中有定義Err()
方法:golang
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()
反饋的錯誤信息僅限於以下兩種:bash
// 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{}
:post
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
的定義和註釋:ui
// 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
裏面會保存什麼嗎?this
...
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
是有必定「偏見」的,但客觀上,它仍是具有一些優勢和功勞的。
許多文章都說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
使用建議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()
}
複製代碼
因此,請忘了「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)
}
複製代碼