Go1-20 新特性:context 支持自定義取消原因

問題

熟悉 Go 語言的同學都知道,context 包只對外提供了兩種取消原因 context.DeadlineExceeded 和 context.Canceled,不支持自定義原因,就像下面這樣:

func main() {
    // Pass a context with a timeout to tell a blocking function that it
    // should abandon its work after the timeout elapses.
    timeoutDuration := 3 * time.Second
    ctx, cancel := context.WithTimeout(context.Background(), timeoutDuration)
    defer cancel()

    // block until context is timed out
    <-ctx.Done()

    switch ctx.Err() {
        case context.DeadlineExceeded:
            fmt.Println("context timeout exceeded")
        case context.Canceled:
            fmt.Println("context cancelled by force")
    }
   
   // output:
   // context timeout exceeded
}

上面的兩種錯誤已經能夠滿足大部分場景,但是如果有時候我想知道更多關於 context 取消的原因就只能額外自定義 error,比如遇到某類取消錯誤時是否需要重試等。

另外,如果是顯示地調用 context.CancelFunc() 函數(上面代碼的 cancel  變量)取消,現在是沒有辦法表明這是否是由於錯誤造成的。

介於此,之前社區就有人提案:如果是顯示取消 context,允許自定義取消原因。

這不它就來了,Go1.20 目前已經支持這一特性。

自定義取消原因

Go1.20 的 context 包提供了 context.WithCancelCause() 支持自定義取消原因,並且提供了提取取消原因的 api:

package main

import (
    "context"
    "errors"
    "fmt"
)

var ErrTemporarilyUnavailable = fmt.Errorf("service temporarily unavailable")

func main() {
    ctx, cancel := context.WithCancelCause(context.Background())

    // operation failed, let's notify the caller by cancelling the context
    cancel(ErrTemporarilyUnavailable)

    switch ctx.Err() {
    case context.Canceled:
        fmt.Println("context cancelled by force")
    }

    // get the cause of cancellation, in this case the ErrTemporarilyUnavailable error
    err := context.Cause(ctx)

    if errors.Is(err, ErrTemporarilyUnavailable) {
        fmt.Printf("cancallation reason: %s", err)
    }
    
    // cancallation reason: service temporarily unavailable
}

上面的代碼,在取消的時候傳入了自定義錯誤 “ErrTemporarilyUnavailable”,並且使用 context.Cause() 提取錯誤原因。

有了這一特性,以後就可以基於取消原因,做更近一步的邏輯操作了,比如是否需要重試等。

更進一步

跟着 context.WithCancelCause() 之後,目前有最新的提案,支持 WithDeadlineCause 和 WithTimeoutCause,目前該提案已經被官方接受,正在開發。

我們先來嚐鮮,看下這兩個分別怎麼用?

context.WithTimeoutCause()

var ErrFailure = fmt.Errorf("request took too long")

func main() {
    timeout := time.Duration(2 * time.Second)
    ctx, _ := context.WithTimeoutCause(context.Background(), timeout, ErrFailure)

    // wait for the context to timeout
    <-ctx.Done()

    switch ctx.Err() {
    case context.DeadlineExceeded:
        fmt.Printf("operation could not complete: %s", context.Cause(ctx))
    }
        
    // operation could not complete: request took too long 
}

context.WithDeadlineCause()

var ErrFailure = fmt.Errorf("request took too long")

func main() {
    timeout := time.Now().Add(time.Duration(2 * time.Second))
    ctx, _ := context.WithDeadlineCause(context.Background(), timeout, ErrFailure)

    // wait for the context to timeout
    <-ctx.Done()

    switch ctx.Err() {
    case context.DeadlineExceeded:
        fmt.Printf("operation could not complete: %s", context.Cause(ctx))
    }
        
    // operation could not complete: request took too long 
}

這些新特性有沒有給你編程帶來便利呢?歡迎留言 “開噴”!


我爲大家整理了一份從入門到進階的 Go 學習資料禮包,包含學習建議:入門看什麼,進階看什麼。關注公衆號 「polarisxu」,回覆 ebook 獲取;還可以回覆「進羣」,和數萬 Gopher 交流學習。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/HbFVODpDEzEZ047kv-LYbg