<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          走進Golang之Context的使用

          共 1016字,需瀏覽 3分鐘

           ·

          2020-08-05 00:44

          我們?yōu)槭裁葱枰?Context 的呢?我們來看看看一個 HTTP 請求的處理:

          請求示意

          例子大概意思是說,有一個獲取訂單詳情的請求,會單獨起一個 goroutine 去處理該請求。在該請求內部又有三個分支 goroutine 分別處理訂單詳情、推薦商品、物流信息;每個分支可能又需要單獨調用DB、Redis等存儲組件。那么面對這個場景我們需要哪些額外的事情呢?

          1. 三個分支 goroutine 可能是對應的三個不同服務,我們想要攜帶一些基礎信息過去,比如:LogID、UserID、IP等;
          2. 每個分支我們需要設置過期時間,如果某個超時不影響整個流程;
          3. 如果主 goroutine 發(fā)生錯誤,取消了請求,對應的三個分支應該也都取消,避免資源浪費;

          簡單歸納就是傳值、同步信號(取消、超時)。

          看到這里可能有人要叫了,完全可以用 channel 來搞?。∧敲次覀兛纯?channel 是否可以滿足。想一個問題,如果是微服務架構,channel 怎么實現(xiàn)跨進程的邊界呢?另外一個問題,就算不跨進程,如果嵌套很多個分支,想一想這個消息傳遞的復雜度。

          如果是你,要實現(xiàn)上面的這個需求,你會怎么做?

          Context 出場

          幸好,我們不用自己每次寫代碼都要去實現(xiàn)這個很基礎的能力。Golang 為我們準備好了一切,就是 context.Context 這個包,這個包的源代碼非常簡單,源碼部分本文會略過,下期單獨一篇文章來講,本篇我們重點談正確的使用。

          Context 的結構非常簡單,它是一個接口。


          // Context 提供跨越API的截止時間獲取,取消信號,以及請求范圍值的功能。
          //?它的這些方案在多個?goroutine?中使用是安全的
          type?Context?interface?{
          ????//?如果設置了截止時間,這個方法ok會是true,并返回設置的截止時間
          ?Deadline()?(deadline?time.Time,?ok?bool)

          ????//?如果?Context?超時或者主動取消返回一個關閉的channel,如果返回的是nil,表示這個
          ????// context 永遠不會關閉,比如:Background()
          ?Done()?<-chan?struct{}

          ????//?返回發(fā)生的錯誤
          ?Err()?error

          ????//?它的作用就是傳值
          ?Value(key?interface{})?interface{}
          }

          寫到這里,我們打住想一想,如果你來實現(xiàn)這樣一個能力的 package,你抽象的接口是否也是具備這樣四個能力?

          • 獲取截止時間
          • 獲取信號
          • 獲取信號產(chǎn)生的對應錯誤信息
          • 傳值專用

          net/http 中是怎么用 context的?

          在我們開始自己鼓搗前,我們先看看 net/http 這個包是怎么使用的。

          func?main()?{
          ?req,?_?:=?http.NewRequest("GET",?"https://api.github.com/users/helei112g",?nil)

          ?//?這里設置了超時時間
          ?ctx,?cancel?:=?context.WithTimeout(context.Background(),?time.Millisecond*1)
          ?defer?cancel()
          ?req?=?req.WithContext(ctx)

          ?resp,?err?:=?http.DefaultClient.Do(req)
          ?if?err?!=?nil?{
          ??log.Fatalln("request?Err",?err.Error())
          ?}
          ?defer?resp.Body.Close()

          ?body,?_?:=?ioutil.ReadAll(resp.Body)
          ?fmt.Println(string(body))
          }

          上面這段程序就是請求 github 獲取用戶信息的接口,通過 context 包設置了請求超時時間是 1ms (肯定無法訪問到)。執(zhí)行時我們看到控制臺做如下輸出:

          2020/xx/xx xx:xx:xx request Err Get https://api.github.com/users/helei112g: context deadline exceeded
          exit status 1

          我們繼續(xù)做實驗,將上面的代碼稍作修改。

          func?main()?{
          ????req,?_?:=?http.NewRequest("GET",?"https://api.github.com/users/helei112g",?nil)

          ????//?這里超時改成了?10s,怎么都夠了吧
          ????ctx,?cancel?:=?context.WithTimeout(context.Background(),?time.Second*10)
          ????//?但是這里移出了?defer?關鍵字
          ? cancel()
          ? req?=?req.WithContext(ctx)

          ????//?沒有改動的部分,省略
          ????...?...
          }

          大家猜猜看能否獲取到請求結果?肯定是不能的,因為 context 取消的信號,在 net/http 包內部通過 ctx.Done() 是能夠拿到的,一旦獲取到就會進行取消。上面的代碼,控制臺會輸出:

          2020/xx/xx xx:xx:xx request Err Get https://api.github.com/users/helei112g: context canceled
          exit status 1

          注意兩次控制臺輸出的錯誤信息是不一樣的。

          • context deadline exceeded 表示執(zhí)行超時被取消了
          • context canceled 表示主動取消

          net/http 中 context 獲取取消信號

          接下來,我們去看看 net/http 包內部是怎么捕捉信號的,我們只關注 context 的部分,其它的直接忽略,源碼路徑如下;

          net/http/transport.go (go 1.13.7)

          //?req?就是我們上面?zhèn)鬟M來的?req,它有個?context?字段
          func?(t?*Transport)?roundTrip(req?*Request)?(*Response,?error)?{
          ?t.nextProtoOnce.Do(t.onceSetNextProtoDefaults)
          ?ctx?:=?req.Context()?//?獲取了?context
          ?trace?:=?httptrace.ContextClientTrace(ctx)?//?這里內部實際用到了?context.Value()?方法

          ?//?各種處理,無關代碼刪除了

          ?//?處理請求
          ?for?{
          ??//?檢查是否關閉了,如果關閉了就直接返回
          ??select?{
          ??case?<-ctx.Done():
          ???req.closeBody()
          ???return?nil,?ctx.Err()
          ??default:
          ??}

          ??//?發(fā)送請求出去
          ?}
          }

          來總結下上面這段代碼,實際上關于 context 的精髓就在 for 循環(huán)中的 select,它通過 ctx.Done() 來獲取信號,因為不管是自動超時,還是主動取消,ctx.Done() 都會收到一個關閉的 channel 的信號。

          這里隱藏了一個細節(jié),那就是如果按照上面的邏輯只能處理到發(fā)起請求前的超時,但是如果請求已經(jīng)被發(fā)出去了,等待這段時間的超時該如何控制呢?感興趣的小伙伴可以去看源碼的這里:

          net/http/transport.go:1234 (go 1.13.7)

          其實就是在內部等待返回的時候不斷的檢查 ctx.Done() 信號,如果發(fā)現(xiàn)了就立即返回。

          好了,官方的技巧我們已經(jīng)學完了,現(xiàn)在輪到我們把開頭的例子寫個代碼來實現(xiàn)下。

          多個 goroutine 控制超時及傳值

          由于服務內部不方便模擬,我們簡化成函數(shù)調用,假設圖中所有的邏輯都可以并發(fā)調用?,F(xiàn)在我們的要求是:

          1. 整個函數(shù)的超時時間為1s;
          2. 需要從最外層傳遞 LogID/UserID/IP 信息到其它函數(shù);
          3. 獲取訂單接口超時為 500ms,由于 DB/Redis 是其內部支持的,這里不進行模擬;
          4. 獲取推薦超時是 400ms;
          5. 獲取物流超時是 700ms。

          為了清晰,我這里所有接口都返回一個字符串,實際中會根據(jù)需要返回不同的結果;請求參數(shù)也都只使用了 context。代碼如下:

          type?key?int

          const?(
          ?userIP?=?iota
          ?userID
          ?logID
          )

          type?Result?struct?{
          ?order?????string
          ?logistics?string
          ?recommend?string
          }

          //?timeout:?1s
          //?入口函數(shù)
          func?api()?(result?*Result,?err?error)?{
          ?ctx,?cancel?:=?context.WithTimeout(context.Background(),?time.Second*1)
          ?defer?cancel()

          ?//?設置值
          ?ctx?=?context.WithValue(ctx,?userIP,?"127.0.0.1")
          ?ctx?=?context.WithValue(ctx,?userID,?666888)
          ?ctx?=?context.WithValue(ctx,?logID,?"123456")

          ?result?=?&Result{}
          ?//?業(yè)務邏輯處理放到協(xié)程中
          ?go?func()?{
          ??result.order,?err?=?getOrderDetail(ctx)
          ?}()
          ?go?func()?{
          ??result.logistics,?err?=?getLogisticsDetail(ctx)
          ?}()
          ?go?func()?{
          ??result.recommend,?err?=?getRecommend(ctx)
          ?}()

          ?for?{
          ??select?{
          ??case?<-ctx.Done():
          ???return?result,?ctx.Err()?//?取消或者超時,把現(xiàn)有已經(jīng)拿到的結果返回
          ??default:

          ??}

          ??//?有錯誤直接返回
          ??if?err?!=?nil?{
          ???return?result,?err
          ??}

          ??//?全部處理完成,直接返回
          ??if?result.order?!=?""?&&?result.logistics?!=?""?&&?result.recommend?!=?""?{
          ???return?result,?nil
          ??}
          ?}
          }

          //?timeout:?500ms
          func?getOrderDetail(ctx?context.Context)?(string,?error)?{
          ?ctx,?cancel?:=?context.WithTimeout(ctx,?time.Millisecond*500)
          ?defer?cancel()

          ?//?模擬超時
          ?time.Sleep(time.Millisecond?*?700)

          ?//?獲取?user?id
          ?uip?:=?ctx.Value(userIP).(string)
          ?fmt.Println("userIP",?uip)

          ?return?handleTimeout(ctx,?func()?string?{
          ??return?"order"
          ?})
          }

          //?timeout:?700ms
          func?getLogisticsDetail(ctx?context.Context)?(string,?error)?{
          ?ctx,?cancel?:=?context.WithTimeout(ctx,?time.Millisecond*700)
          ?defer?cancel()

          ?//?獲取?user?id
          ?uid?:=?ctx.Value(userID).(int)
          ?fmt.Println("userID",?uid)

          ?return?handleTimeout(ctx,?func()?string?{
          ??return?"logistics"
          ?})
          }

          //?timeout:?400ms
          func?getRecommend(ctx?context.Context)?(string,?error)?{
          ?ctx,?cancel?:=?context.WithTimeout(ctx,?time.Millisecond*400)
          ?defer?cancel()

          ?//?獲取?log?id
          ?lid?:=?ctx.Value(logID).(string)
          ?fmt.Println("logID",?lid)

          ?return?handleTimeout(ctx,?func()?string?{
          ??return?"recommend"
          ?})
          }

          //?超時的統(tǒng)一處理代碼
          func?handleTimeout(ctx?context.Context,?f?func()?string)?(string,?error)?{
          ?//?請求之前先去檢查下是否超時
          ?select?{
          ?case?<-ctx.Done():
          ??return?"",?ctx.Err()
          ?default:
          ?}

          ?str?:=?make(chan?string)
          ?go?func()?{
          ??//?業(yè)務邏輯
          ??str?<-?f()
          ?}()

          ?select?{
          ?case?<-ctx.Done():
          ??return?"",?ctx.Err()
          ?case?ret?:=?<-str:
          ??return?ret,?nil
          ?}
          }

          不知道你是否看明白了整個使用,我們這個例子看起來很復雜,實際上與我給你介紹的 net/http 包控制超時是一樣的,只不過 net/http 的控制超時代碼不需要我們寫,而且我們這里一次性把三個調用的整合到了一起。

          還有一點說明一下,對于 select,如果沒有寫 defalut 分支,是不需要放在 for 循環(huán)中的,因為它本身就會阻塞(網(wǎng)絡上有很多例子放在for循環(huán)中)。

          參考資料

          • [1] Package context
          • [2] Go Concurrency Patterns: Context




          推薦閱讀



          學習交流 Go 語言,掃碼回復「進群」即可


          站長 polarisxu

          自己的原創(chuàng)文章

          不限于 Go 技術

          職場和創(chuàng)業(yè)經(jīng)驗


          Go語言中文網(wǎng)

          每天為你

          分享 Go 知識

          Go愛好者值得關注


          瀏覽 47
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  国产女人操逼 | 亚洲天堂网在线视频 | 麻豆成人免费视频在线观看 | 亲子乱AⅤ一区二区三区 | 豆花视频成人版www满18 |