go-context筆記

WHY

每一個長請求都應(yīng)該有個超時限制
需要在調(diào)用中傳遞這個超時
比如開始處理請求的時候我們說是 3 秒鐘超時
那么在函數(shù)調(diào)用中間,這個超時還剩多少時間了?
需要在什么地方存儲這個信息,這樣請求處理中間可以停止

context

Context通常被譯作上下文。
一般理解為程序單元的一個運行狀態(tài)、現(xiàn)場、快照,而翻譯中上下又很好地詮釋了其本質(zhì),上下上下則是存在上下層的傳遞,上會把內(nèi)容傳遞給下。在Go語言中,程序單元也就指的是Goroutine。
為了支持多Goroutine管理機制。
每個Goroutine在執(zhí)行之前,都要先知道程序當(dāng)前的執(zhí)行狀態(tài),通常將這些執(zhí)行狀態(tài)封裝在一個Context變量中,傳遞給要執(zhí)行的Goroutine中。上下文則幾乎已經(jīng)成為傳遞與請求同生存周期變量的標準方法。在網(wǎng)絡(luò)編程下,當(dāng)接收到一個網(wǎng)絡(luò)請求Request,處理Request時,我們可能需要開啟不同的Goroutine來獲取數(shù)據(jù)與邏輯處理,即一個請求Request,會在多個Goroutine中處理。而這些Goroutine可能需要共享Request的一些信息;同時當(dāng)Request被取消或者超時的時候,所有從這個Request創(chuàng)建的所有Goroutine也應(yīng)該被結(jié)束。

實現(xiàn)

  • context.Context:
    是不可變的(immutable)樹節(jié)點
    Cancel 一個節(jié)點,會連帶 Cancel 其所有子節(jié)點 (從上到下)
    Context values 是一個節(jié)點
    Value 查找是回溯樹的方式 (從下到上)
  • context鏈
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.Context API
    3個函數(shù)用于限定什么時候你的子節(jié)點退出;
    1個函數(shù)用于設(shè)置請求范疇的變量
type Context interface {
    Deadline() (deadline time.Time, ok bool)
    Done() <-chan struct{}
    Err() error
    Value(key interface{}) interface{}
}

Deadline會返回一個超時時間,Goroutine獲得了超時時間后,例如可以對某些io操作設(shè)定超時時間。

Done方法返回一個信道(channel),當(dāng)Context被撤銷或過期時,該信道是關(guān)閉的,即它是一個表示Context是否已關(guān)閉的信號。

當(dāng)Done信道關(guān)閉后,Err方法表明Context被撤的原因。

Value可以讓Goroutine共享一些數(shù)據(jù),當(dāng)然獲得數(shù)據(jù)是協(xié)程安全的。但使用這些數(shù)據(jù)的時候要注意同步,比如返回了一個map,而這個map的讀寫則要加鎖。

函數(shù)、接口、結(jié)構(gòu)體

image.png

創(chuàng)建 Context

  • 在 RPC 開始的時候,使用 context.Background()
    有些人把在 main() 里記錄一個 context.Background(),然后把這個放到服務(wù)器的某個變量里,然后請求來了后從這個變量里繼承 context。這么做是不對的。直接每個請求,源自自己的 context.Background() 即可。
  • 如果你沒有 context,卻需要調(diào)用一個 context 的函數(shù)的話,用 context.TODO()
  • 如果某步操作需要自己的超時設(shè)置的話,給它一個獨立的 sub-context(如前面的例子)

集成到API里

  • 如果有 Context,將其作為第一個變量。
func (d* Dialer) DialContext(ctx context.Context, network, address string) (Conn, error)

request 結(jié)構(gòu)體應(yīng)該以 Request 結(jié)束為生命終止, 當(dāng) RPC 請求處理結(jié)束后,應(yīng)該去掉對 Context 變量的引用(Unreference)

  • 及時關(guān)閉
    ctx, cancel := context.WithTimeout(parentCtx, time.Second * 2)
    defer cancel()

context.Value API 的萬金油(duct tape)

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 樹形結(jié)構(gòu)中,增加一個節(jié)點.

約束 key 的空間

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 里某個值,而是產(chǎn)生新的 Context 帶某個值。

  • Context.Value 是 immutable 的,context.Context 從設(shè)計上就是按照 immutable (不可變的)模式設(shè)計的,同樣,Context.Value 也是 immutable 的。
  • Context.Value 與request同生共死。
    Request 數(shù)據(jù)衍生出來,并且隨著 Request 的結(jié)束而終結(jié)
  • 注意
    對于調(diào)試非常方便
    將必須的信息放入 Context.Value 中,會讓接口定義更加不透明
    如果可以盡量明確定義在接口
    盡量不要用 Context.Value

參考

機制 https://www.cnblogs.com/zhangboyu/p/7456606.html
https://zhuanlan.zhihu.com/p/68792989
http://www.itdecent.cn/p/ca7f0629de77
https://studygolang.com/articles/23247?fr=sidebar

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容