How to correctly use package context by Jack Lindamood at Golang UK Conf. 2017html
視頻:www.youtube.com/watch?v=-_B… 博文:medium.com/@cep21/how-…git
若是進一步考慮。github
這是正常的方式,可是若是 RPC 2 調用失敗了會發生什麼?golang
RPC 2 失敗後,若是沒有 Context 的存在,那麼咱們可能依舊會等全部的 RPC 執行完畢,可是因爲 RPC 2 敗了,因此其實其它的 RPC 結果意義不大了,咱們依舊須要給用戶返回錯誤。所以咱們白白的浪費了 10ms,徹底不必去等待其它 RPC 執行完畢。數據庫
那若是咱們在 RPC 2 失敗後,就直接給用戶返回失敗呢?安全
因此理想狀態應該是如上圖,當 RPC 2 出錯後,除了返回用戶錯誤信息外,咱們也應該有某種方式能夠通知 RPC 3 和 RPC 4,讓他們也中止運行,再也不浪費資源。bash
因此解決方案就是:服務器
那乾脆讓咱們把變量也扔那吧。😈微信
context.Context:併發
完整代碼:play.golang.org/p/ddpofBV1Q…
package main
func tree() {
ctx1 := context.Background()
ctx2, _ := context.WithCancel(ctx1)
ctx3, _ := context.WithTimeout(ctx2, time.Second * 5)
ctx4, _ := context.WithTimeout(ctx3, time.Second * 3)
ctx5, _ := context.WithTimeout(ctx3, time.Second * 6)
ctx6 := context.WithValue(ctx5, "userID", 12)
}
複製代碼
若是這樣構成的 Context 鏈,其形以下圖:
當 5秒鐘 超時到達時:
基本上是兩類操做:
type Context interface {
// 啥時候退出
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
// 設置變量
Value(key interface{}) interface{}
}
複製代碼
ctx, cancel := context.WithTimeout(parentCtx, time.Second * 2)
defer cancel()
複製代碼
當你再也不關心接下來獲取的結果的時候,有可能會 Cancel 一個 Context?
以 golang.org/x/sync/errgroup 爲例,errgroup 使用 Context 來提供 RPC 的終止行爲。
type Group struct {
cancel func()
wg sync.WaitGroup
errOnce sync.Once
err error
}
複製代碼
建立一個 group 和 context:
func WithContext(ctx context.Context) (*Group, context.Context) {
ctx, cancel := context.WithCancel(ctx)
return &Group{cancel: cancel}, ctx
}
複製代碼
這樣就返回了一個能夠被提早 cancel 的 group。
而調用的時候,並非直接調用 go func(),而是調用 Go(),將函數做爲參數傳進去,用高階函數的形式來調用,其內部纔是 go func() 開啓 goroutine。
func (g *Group) Go(f func() error) {
g.wg.Add(1)
go func() {
defer g.wg.Done()
if err := f(); err != nil {
g.errOnce.Do(func() {
g.err = err
if g.cancel != nil {
g.cancel()
}
})
}
}()
}
複製代碼
當給入函數 f 返回錯誤,則使用 sync.Once 來 cancel context,而錯誤被保存於 g.err 之中,在隨後的 Wait() 函數中返回。
func (g *Group) Wait() error {
g.wg.Wait()
if g.cancel != nil {
g.cancel()
}
return g.err
}
複製代碼
注意:這裏在 Wait() 結束後,調用了一次 cancel()。
package main
func DoTwoRequestsAtOnce(ctx context.Context) error {
eg, egCtx := errgroup.WithContext(ctx)
var resp1, resp2 *http.Response
f := func(loc string, respIn **http.Response) func() error {
return func() error {
reqCtx, cancel := context.WithTimeout(egCtx, time.Second)
defer cancel()
req, _ := http.NewRequest("GET", loc, nil)
var err error
*respIn, err = http.DefaultClient.Do(req.WithContext(reqCtx))
if err == nil && (*respIn).StatusCode >= 500 {
return errors.New("unexpected!")
}
return err
}
}
eg.Go(f("http://localhost:8080/fast_request", &resp1))
eg.Go(f("http://localhost:8080/slow_request", &resp2))
return eg.Wait()
}
複製代碼
在這個例子中,同時發起了兩個 RPC 調用,當任何一個調用超時或者出錯後,會終止另外一個 RPC 調用。這裏就是利用前面講到的 errgroup 來實現的,應對有不少並不是請求,並須要集中處理超時、出錯終止其它併發任務的時候,這個 pattern 使用起來很方便。
膠帶(duct tape) 幾乎能夠修任何東西,從破箱子,到人的傷口,到汽車引擎,甚至到NASA登月任務中的阿波羅13號飛船(Yeah! True Story)。因此在西方文化裏,膠帶是個「萬能」的東西。在中文裏,恐怕萬金油是更合適的對應詞彙,從頭疼、腦熱,感冒發燒,到跌打損傷幾乎無所不治。
固然,治標不治本,這點東西方文化中的潛臺詞都是同樣的。這裏說起的 context.Value 對於 API 而言,就是這類性質的東西,啥均可以幹,可是治標不治本。
package context
type valueCtx struct {
Context
key, val interface{}
}
func WithValue(parent Context, key, val interface{}) Context {
// ...
return &valueCtx{parent, key, val}
}
func (c *valueCtx) Value(key interface{}) interface{} {
if c.key == key {
return c.val
}
return c.Context.Value(key)
}
複製代碼
能夠看到,WithValue() 實際上就是在 Context 樹形結構中,增長一個節點罷了。
Context 是 immutable 的。
爲了防止樹形結構中出現重複的鍵,建議約束鍵的空間。好比使用私有類型,而後用 GetXxx() 和 WithXxxx() 來操做私有實體。
type privateCtxType string
var (
reqID = privateCtxType("req-id")
)
func GetRequestID(ctx context.Context) (int, bool) {
id, exists := ctx.Value(reqID).(int)
return id, exists
}
func WithRequestID(ctx context.Context, reqid int) context.Context {
return context.WithValue(ctx, reqID, reqid)
}
複製代碼
這裏使用 WithXxx 而不是 SetXxx 也是由於 Context 其實是 immutable 的,因此不是修改 Context 裏某個值,而是產生新的 Context 帶某個值。
再屢次的強調 Context.Value 是 immutable 的也不過度。
func Add(ctx context.Context) int {
return ctx.Value("first").(int) + ctx.Value("second").(int)
}
複製代碼
曾經看到過一個 API,就是這種形式:
func IsAdminUser(ctx context.Context) bool {
userID := GetUser(ctx)
return authSingleton.IsAdmin(userID)
}
複製代碼
這裏API實現內部從 context 中取得 UserID,而後再進行權限判斷。可是從函數簽名看,則徹底沒法理解這個函數具體須要什麼、以及作什麼。
代碼要以可讀性爲優先設計考慮。
別人拿到一個代碼,通常不是掉進函數實現細節裏去一行行的讀代碼,而是會先瀏覽一下函數接口。因此清晰的函數接口設計,會更加利於別人(或者是幾個月後的你本身)理解這段代碼。
一個良好的 API 設計,應該從函數簽名就清晰的理解函數的邏輯。若是咱們將上面的接口改成:
func IsAdminUser(ctx context.Context, userID string, authenticator auth.Service) bool
複製代碼
咱們從這個函數簽名就能夠清楚的知道:
全部這些信息,都是從函數簽名獲得的,而無需打開函數實現一行行去看。
如今知道 Context.Value 會讓接口定義更加模糊,彷佛不該該使用。那麼又回到了原來的問題,到底什麼能夠放到 Context.Value 裏去?換個角度去想,什麼不是衍生於 Request?
package main
func trace(req *http.Request, c *http.Client) {
trace := &httptrace.ClientTrace{
GotConn: func(connInfo httptrace.GotConnInfo) {
fmt.Println("Got Conn")
},
ConnectStart: func(network, addr string) {
fmt.Println("Dial Start")
},
ConnectDone: func(network, addr string, err error) {
fmt.Println("Dial done")
},
}
req = req.WithContext(httptrace.WithClientTrace(req.Context(), trace))
c.Do(req)
}
複製代碼
package http
func (req *Request) write(w io.Writer, usingProxy bool, extraHeaders Header, waitForContinue func() bool) (err error) {
// ...
trace := httptrace.ContextClientTrace(req.Context())
// ...
if trace != nil && trace.WroteHeaders != nil {
trace.WroteHeaders()
}
}
複製代碼
package main
import "github.com/golang/oauth2"
func oauth() {
c := &http.Client{Transport: &mockTransport{}}
ctx := context.WithValue(context.Background(), oauth2.HTTPClient, c)
conf := &oauth2.Config{ /* ... */ }
conf.Exchange(ctx, "code")
}
複製代碼
context.Value 並無讓你的 API 更簡潔,那是假象,相反,它讓你的 API 定義更加模糊。
以前說過長時間、可阻塞的操做都用 Context,數據庫操做也是如此。不過對於超時 Cancel 操做來講,通常不會對寫操做進行 cancel;可是對於讀操做,通常會有 Cancel 操做。
我的微信公衆號:
我的github:
我的博客: