網頁

2021/10/2

Golang context 簡介

Go 標準函式庫的context package簡介。


Go context用來在多個goroutine間傳遞資料、超時訊號及取消訊號。

了解context前要先認識其應用場景。Go server收到一個請求後通常會建立其他goroutine做併發處理來提高效能,例如耗時計算處理、從資料庫取得資料或呼叫外部服務。而多個goroutine間即可利用context來存取共用的請求資料。共用的請求資料包括使用者資料、Auth token、請求限制時間、追蹤編號等。


Context規則

使用context應遵守以下規則:

  • 不要在struct的屬性存放Context,在函式間傳遞Context時應明確地放在第一個參數並命名為ctx
    func DoSomething(ctx context.Context, arg1 Arg, arg2 Arg, ...) error {...}
  • 不要傳遞nilContext參數,若不知道Context可用context.TODO()替代。
    DoSomething(context.TODO(), arg1, arg2, ...)
  • Context應只傳遞屬請求範圍的資料(request-scoped data)。
  • 同個Context能在不同的goroutine間傳遞,是執行緒安全的。


Context介面

context package的核心為Context介面,定義了以下方法:

  • Done() <-chan struct{} - 當Context因取消或超時而關閉返回一個關閉的channel receive代表取消訊號、反之返回nil,多用在select敘述做goroutine的關閉處理。
    ctx, cancel := context.WithCancel(ctx)
    ...
    select {
    case <-ctx.Done():
      	...
    }
  • Err() error - 如果Context未關閉返回nil,反之返回錯誤error說明關閉原因。
    ctx, cancel := context.WithCancel(ctx)
    err := ctx.Err()
  • Deadline() (deadline time.Time, ok bool) - 當Context超時時回傳時間且ok==true,無超時則回傳ok==false
    ctx, cancel := context.WithDeadline(ctx, time)
    time, ok := ctx.Deadline()
  • Value(key interface{}) interface{} - 用來取得Conetext中的資料,使用key取得。
    ctx := context.WithValue(ctx, "foo", "bar")
    fmt.Println(ctx.Value("foo")) // bar


建立Context


Root Context

使用context.Background()建立root Context,此為Context tree的根,root Context無法被取消,沒有deadline且不帶value。其他Context都源於root Context。

main.go

package main

import (
    "context"
)

func main() {
    root := context.Background() // create a root Context
    ...
}

Context可形成tree(樹結構),一個Context可衍伸出多個子Context,稱為derived Context。


Derived Context

Derived Context可使用context.WithCancelcontext.WithDeadlinecontext.WithTimeoutcontext.WithValue建立,皆必須接收一個parent Context為參數。


WithCancel(parent Context) (ctx Context, cancel CancelFunc)

接收parent Context參數,回傳一個複製parent Context的Context並有新的Done channel及CancelFunc函式cancel

CancelFunc函式被調用或parent Context的Done channel關閉時,回傳的Context也會關閉。

例如下面當隨機數為0時調用cancel()關閉Context,則Context.Done()返回一個關閉的channel並印出"context closed"。

package main

import (
    "context"
    "fmt"
    "math/rand"
    "time"
)

func main() {

    root := context.Background()
    ctx, cancel := context.WithCancel(root)
    defer cancel()

    rand.Seed(time.Now().UnixNano())
    if rand.Intn(2) == 0 {
        cancel() // close context
    }

    select {
    case <-ctx.Done():
        fmt.Println("context closed")
    default:
        fmt.Println("hello")
    }

}


WithDeadline(parent Context, d time.Time) (ctx Context, cancel CancelFunc)

parent Context為第一參數,第二參數為逾時時間,回傳有Done channel的Context及CancelFunc函式cancel

若超過逾時時間、CancelFunc函式被調用或parent Context的Done channel關閉時,回傳的Context也會關閉。

例如下面執行sleep時若超過context.WithDeadline設定當下時間後1秒時便自動關閉Context的Done channel。

package main

import (
    "context"
    "fmt"
    "math/rand"
    "time"
)

func main() {

    root := context.Background()
    ctx, cancel := context.WithDeadline(root, time.Now().Add(time.Second*1))
    defer cancel()

    rand.Seed(time.Now().UnixNano())
    d := time.Duration(rand.Intn(3))
    fmt.Printf("sleep %d seconds\n", d)
    time.Sleep(time.Second * d)

    select {
    case <-ctx.Done():
        fmt.Println("context closed")
    default:
        fmt.Println("hello")
    }

}


WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)

相當於WithDeadline(parent, time.Now().Add(timeout),設定執行當下的時間往後多久為超時。


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

parent Context為第一參數,第二參數為key,第三個參數val為key對應的值。使用此函式可把資料放入context在goroutine中傳遞。

下面建立三個Context階層依序為ctx -> ctx1 -> ctx2。使用Context.WithValue分別在不同的Context放入值,可看到最末階的ctx2可取得parnet Context塞入的值。此外也可看到parent Context若取消其derived Context也會被關閉。

package main

import (
    "context"
    "fmt"
)

func main() {

    ctx, cancel := context.WithCancel(context.Background())
    ctx1 := context.WithValue(ctx, "hello", "world")
    ctx2 := context.WithValue(ctx1, "foo", "bar")

    cancel()

    select {
    case <-ctx2.Done():
        fmt.Println(ctx2.Value("hello"))
        fmt.Println(ctx2.Value("foo"))
    }

}


沒有留言:

張貼留言