Golang Context的好與壞及使用建議

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

  1. 因取消而取消 (excuse me???)
  2. 因超時而取消
// 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是有必定「偏見」的,但客觀上,它仍是具有一些優勢和功勞的。

統一了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裏面放什麼。

使用NewContextFromContext對來存取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)
}
複製代碼

其餘參考

相關文章
相關標籤/搜索