<kbd id="5sdj3"></kbd>
<th id="5sdj3"></th>

  • <dd id="5sdj3"><form id="5sdj3"></form></dd>
    <td id="5sdj3"><form id="5sdj3"><big id="5sdj3"></big></form></td><del id="5sdj3"></del>

  • <dd id="5sdj3"></dd>
    <dfn id="5sdj3"></dfn>
  • <th id="5sdj3"></th>
    <tfoot id="5sdj3"><menuitem id="5sdj3"></menuitem></tfoot>

  • <td id="5sdj3"><form id="5sdj3"><menu id="5sdj3"></menu></form></td>
  • <kbd id="5sdj3"><form id="5sdj3"></form></kbd>

    一起聊聊 Go Context 的正確使用姿勢(shì)

    共 6425字,需瀏覽 13分鐘

     ·

    2021-07-17 02:26

    大家好,我是煎魚。

    在 Go 語言中,Goroutine(協(xié)程),也就是關(guān)鍵字 go 是一個(gè)家喻戶曉的高級(jí)用法。這起的非常妙,說到 Go,就會(huì)想到這一門語言,想到 goroutine 這一關(guān)鍵字,而與之關(guān)聯(lián)最深的就是 context。

    背景

    平時(shí)在 Go 工程中開發(fā)中,幾乎所有服務(wù)端(例如:HTTP Server)的默認(rèn)實(shí)現(xiàn),都在處理請(qǐng)求時(shí)新起 goroutine 進(jìn)行處理。

    但一開始存在一個(gè)問題,那就是當(dāng)一個(gè)請(qǐng)求被取消或超時(shí)時(shí),所有在該請(qǐng)求上工作的 goroutines 應(yīng)該迅速退出,以便系統(tǒng)可以回收他們正在使用的任何資源。

    當(dāng)年可沒有 context 標(biāo)準(zhǔn)庫。很折騰。因此 Go 官方在 2014 年正式宣發(fā)了 context 標(biāo)準(zhǔn)庫,形成一個(gè)完整的閉環(huán)。

    但有了 context 標(biāo)準(zhǔn)庫,Go 愛好者們又奇怪了,前段時(shí)間我就被問到了:“Go context 的正確使用姿勢(shì)是怎么樣的”?

    (一張忘記在哪里被問的隱形截圖)

    今天這篇文章就由煎魚帶你看看。

    Context 用法

    在 Go context 用法中,我們常常將其與 select 關(guān)鍵字結(jié)合使用,用于監(jiān)聽其是否結(jié)束、取消等。

    代碼如下:

    const shortDuration = 1 * time.Millisecond

    func main() {
     ctx, cancel := context.WithTimeout(context.Background(), shortDuration)
     defer cancel()

     select {
     case <-time.After(1 * time.Second):
      fmt.Println("腦子進(jìn)煎魚了")
     case <-ctx.Done():
      fmt.Println(ctx.Err())
     }
    }

    輸出結(jié)果:

    context deadline exceeded

    如果是更進(jìn)一步結(jié)合 goroutine 的話,常見的例子是:

     func(ctx context.Context) <-chan int {
      dst := make(chan int)
      n := 1
      go func() {
       for {
        select {
        case <-ctx.Done():
         return
        case dst <- n:
         n++
        }
       }
      }()
      return dst
     }

    我們平時(shí)工程中會(huì)起很多的 goroutine,這時(shí)候會(huì)在 goroutine 內(nèi)結(jié)合 for+select,針對(duì) context 的事件進(jìn)行處理,達(dá)到跨 goroutine 控制的目的。

    正確的使用姿勢(shì)

    對(duì)第三方調(diào)用要傳入 context

    在 Go 語言中,Context 的默認(rèn)支持已經(jīng)是約定俗稱的規(guī)范了。因此在我們對(duì)第三方有調(diào)用訴求的時(shí)候,要傳入 context:

    func main() {
     req, err := http.NewRequest("GET""https://eddycjy.com/"nil)
     if err != nil {
      fmt.Printf("http.NewRequest err: %+v", err)
      return
     }

     ctx, cancel := context.WithTimeout(req.Context(), 50*time.Millisecond)
     defer cancel()

     req = req.WithContext(ctx)
     resp, err := http.DefaultClient.Do(req)
     if err != nil {
      fmt.Printf("http.DefaultClient.Do err: %+v", err)
      return
     }
     defer resp.Body.Close()
    }

    這樣子由于第三方開源庫已經(jīng)實(shí)現(xiàn)了根據(jù) context 的超時(shí)控制,那么當(dāng)你所傳入的時(shí)間到達(dá)時(shí),將會(huì)中斷調(diào)用。

    若你發(fā)現(xiàn)第三方開源庫沒支持 context,那建議趕緊跑,換一個(gè)。免得在微服務(wù)體系下出現(xiàn)級(jí)聯(lián)故障,還沒有簡單的手段控制,那就很麻煩了。

    不要將上下文存儲(chǔ)在結(jié)構(gòu)類型中

    大家會(huì)發(fā)現(xiàn),在 Go 語言中,所有的第三方開源庫,業(yè)務(wù)代碼。清一色的都會(huì)將 context 放在方法的一個(gè)入?yún)?shù),作為首位形參。

    例如:

    標(biāo)準(zhǔn)要求:每個(gè)方法的第一個(gè)參數(shù)都將 context 作為第一個(gè)參數(shù),并使用 ctx 變量名慣用語。

    當(dāng)然,我們也不能一桿子打死所有情況。確實(shí)存在極少數(shù)是把 context 放在結(jié)構(gòu)體中的。基本常見于:

    • 底層基礎(chǔ)庫。
    • DDD 結(jié)構(gòu)。

    每個(gè)請(qǐng)求都是獨(dú)立的,context 自然每個(gè)都不一樣,想清楚自己的應(yīng)用使用場(chǎng)景很重要,否則遵循 Go 基本規(guī)范就好。

    在真實(shí)案例來看,有的 Leader 會(huì)單純?yōu)榱瞬幌腩l繁傳 context 而設(shè)計(jì)成結(jié)構(gòu)體,結(jié)果導(dǎo)致一線 RD 就得天天 NewXXX,甚至有時(shí)候忘記了,還得背個(gè)小鍋。

    函數(shù)調(diào)用鏈必須傳播上下文

    我們會(huì)把 context 作為方法首位,本質(zhì)目的是為了傳播 context,自行完整調(diào)用鏈路上的各類控制:

    func List(ctx context.Context, db *sqlx.DB) ([]User, error) {
     ctx, span := trace.StartSpan(ctx, "internal.user.List")
     defer span.End()

     users := []User{}
     const q = `SELECT * FROM users`

     if err := db.SelectContext(ctx, &users, q); err != nil {
      return nil, errors.Wrap(err, "selecting users")
     }

     return users, nil
    }

    像在上述例子中,我們會(huì)把所傳入方法的 context 一層層的傳進(jìn)去下一級(jí)方法。這里就是將外部的 context 傳入 List 方法,再傳入 SQL 執(zhí)行的方法,解決了 SQL 執(zhí)行語句的時(shí)間問題。

    context 的繼承和派生

    在 Go 標(biāo)準(zhǔn)庫 context 中具有以下派生 context 的標(biāo)準(zhǔn)方法:

    func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
    func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)
    func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)

    代碼例子如下:

    func handle(w http.ResponseWriter, req *http.Request) {
      // parent context
     timeout, _ := time.ParseDuration(req.FormValue("timeout"))
     ctx, cancel := context.WithTimeout(context.Background(), timeout)

      // chidren context
     newCtx, cancel := context.WithCancel(ctx)
     defer cancel()
     // do something...
    }

    一般會(huì)有父級(jí) context 和子級(jí) context 的區(qū)別,我們要保證在程序的行為中上下文對(duì)于多個(gè) goroutine 同時(shí)使用是安全的。并且存在父子級(jí)別關(guān)系,父級(jí) context 關(guān)閉或超時(shí),可以繼而影響到子級(jí) context 的程序。

    不傳遞 nil context

    很多時(shí)候我們?cè)趧?chuàng)建 context 時(shí),還不知道其具體的作用和下一步用途是什么。

    這種時(shí)候大家可能會(huì)直接使用 context.Background 方法:

    var (
       background = new(emptyCtx)
       todo       = new(emptyCtx)
    )

    func Background() Context {
       return background
    }

    func TODO() Context {
       return todo
    }

    但在實(shí)際的 context 建議中,我們會(huì)建議使用 context.TODO 方法來創(chuàng)建頂級(jí)的 context,直到弄清楚實(shí)際 Context 的下一步用途,再進(jìn)行變更。

    context 僅傳遞必要的值

    我們?cè)谑褂?context 作為上下文時(shí),經(jīng)常有信息傳遞的訴求。像是在 gRPC 中就會(huì)有 metadata 的概念,而在 gin 中就會(huì)自己封裝 context 作為參數(shù)管理。

    Go 標(biāo)準(zhǔn)庫 context 也有提供相關(guān)的方法:

    type Context
        func WithValue(parent Context, key, val interface{}) Context

    代碼例子如下:

    func main() {
     type favContextKey string
     f := func(ctx context.Context, k favContextKey) {
      if v := ctx.Value(k); v != nil {
       fmt.Println("found value:", v)
       return
      }
      fmt.Println("key not found:", k)
     }

     k := favContextKey("腦子進(jìn)")
     ctx := context.WithValue(context.Background(), k, "煎魚")

     f(ctx, k)
     f(ctx, favContextKey("小咸魚"))
    }

    輸出結(jié)果:

    found value: 煎魚
    key not found: 小咸魚

    在規(guī)范中,我們建議 context 在傳遞時(shí),僅攜帶必要的參數(shù)給予其他的方法,或是 goroutine。甚至在 gRPC 中會(huì)做嚴(yán)格的出、入上下文參數(shù)的控制。

    在業(yè)務(wù)場(chǎng)景上,context 傳值適用于傳必要的業(yè)務(wù)核心屬性,例如:租戶號(hào)、小程序ID 等。不要將可選參數(shù)放到 context 中,否則可能會(huì)一團(tuán)糟。

    總結(jié)

    • 對(duì)第三方調(diào)用要傳入 context,用于控制遠(yuǎn)程調(diào)用。
    • 不要將上下文存儲(chǔ)在結(jié)構(gòu)類型中,盡可能的作為函數(shù)第一位形參傳入。
    • 函數(shù)調(diào)用鏈必須傳播上下文,實(shí)現(xiàn)完整鏈路上的控制。
    • context 的繼承和派生,保證父、子級(jí) context 的聯(lián)動(dòng)。
    • 不傳遞 nil context,不確定的 context 應(yīng)當(dāng)使用 TODO。
    • context 僅傳遞必要的值,不要讓可選參數(shù)揉在一起。


    關(guān)注煎魚,吸取他的知識(shí) ??



    你好,我是煎魚。高一折騰過前端,參加過國賽拿了獎(jiǎng),大學(xué)搞過 PHP?,F(xiàn)在整 Go,在公司負(fù)責(zé)微服務(wù)架構(gòu)等相關(guān)工作推進(jìn)和研發(fā)。

    從大學(xué)開始靠自己賺生活費(fèi)和學(xué)費(fèi),到出版 Go 暢銷書《Go 語言編程之旅》,再到獲得 GOP(Go 領(lǐng)域最有觀點(diǎn)專家)榮譽(yù),點(diǎn)擊藍(lán)字查看我的出書之路。

    日常分享高質(zhì)量文章,輸出 Go 面試、工作經(jīng)驗(yàn)、架構(gòu)設(shè)計(jì),加微信拉讀者交流群,記得點(diǎn)贊!

    瀏覽 40
    點(diǎn)贊
    評(píng)論
    收藏
    分享

    手機(jī)掃一掃分享

    分享
    舉報(bào)
    評(píng)論
    圖片
    表情
    推薦
    點(diǎn)贊
    評(píng)論
    收藏
    分享

    手機(jī)掃一掃分享

    分享
    舉報(bào)

    <kbd id="5sdj3"></kbd>
    <th id="5sdj3"></th>

  • <dd id="5sdj3"><form id="5sdj3"></form></dd>
    <td id="5sdj3"><form id="5sdj3"><big id="5sdj3"></big></form></td><del id="5sdj3"></del>

  • <dd id="5sdj3"></dd>
    <dfn id="5sdj3"></dfn>
  • <th id="5sdj3"></th>
    <tfoot id="5sdj3"><menuitem id="5sdj3"></menuitem></tfoot>

  • <td id="5sdj3"><form id="5sdj3"><menu id="5sdj3"></menu></form></td>
  • <kbd id="5sdj3"><form id="5sdj3"></form></kbd>
    国产女人操逼视频 | 亚洲三区视频 | 黄色视频网站日本大全免费看 | 中国黄色A片 | 中国美女正在操b |