LoginSignup
0
2

More than 3 years have passed since last update.

【Go言語】contextパッケージを理解する-実践編-

Last updated at Posted at 2020-08-12

実際にContextを使ってみる

キャンセルの伝播


package main

import (
    "context"
    "fmt"
    "time"
)

func main() {
    // contextを生成
    ctx := context.Background()

    ctxParent, cancel := context.WithCancel(ctx)
    go parent(ctxParent, "Hello-parent")
    time.Sleep(5 * time.Second)

    cancel()
        time.Sleep(1 * time.Second)
    fmt.Println("main end")
}

func parent(ctx context.Context, str string) {
    childCtx, cancel := context.WithCancel(ctx)
    go child(childCtx, "Hello-child")
    defer cancel()
    // 無限ループ
    for i := 1; i <= 1000; i++ {
        select {
        case <-ctx.Done():
            fmt.Println(ctx.Err(), str)
            return

        case <-time.After(1 * time.Second):
            fmt.Printf("%s:%d sec..\n", str, i)
        }
    }
}

func child(ctx context.Context, str string) {
    // 無限ループ
    for i := 1; i <= 1000; i++ {
        select {
        case <-ctx.Done():
            fmt.Println(ctx.Err(), str)
            return
        case <-time.After(1 * time.Second):
            fmt.Printf("%s:%d sec..\n", str, i)
        }
    }
}

もちろんWithDeadlineやwithTimeoutを使うと明示的に指定した時間以上にかかっている場合にその処理をキャンセルすることが可能。

context導入によるコード比較

context利用なし

func handler(w http.ResponseWriter, r *http.Request) {
    doneCh := make(chan struct{}, 1)

    errCh := make(chan error, 1)
    go func() {
        errCh <- request(doneCh)
    }()

    // 別途goroutineを準備してTimeoutを設定する
    go func() {
        <-time.After(2 * time.Second)
        // Timeout後にdoneChをクローズする
        // 参考: https://blog.golang.org/pipelines
        close(doneCh)
    }()

    select {
    case err := <-errCh:
        if err != nil {
            log.Println("failed:", err)
            return
        }
    }

    log.Println("success")
}


func request(doneCh chan struct{}) error {
    tr := &http.Transport{}
    client := &http.Client{Transport: tr}

    req, err := http.NewRequest("POST", backendService, nil)
    if err != nil {
        return err
    }
  
   errCh := make(chan error, 1)
    go func() {
        _, err := client.Do(req)
        errCh <- err
    }()

    select {
    case err := <-errCh:
        if err != nil {
            return err
        }


    case <-doneCh:
        // キャンセルが実行されたら適切にリクエストを停止して
        // エラーを返す.
        tr.CancelRequest(req)
        <-errCh
        return fmt.Errorf("canceled")
    }

    return nil
}

context利用


func handler(w http.ResponseWriter, r *http.Request) {

    ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
    defer cancel()

    errCh := make(chan error, 1)
    go func() {
        errCh <- request3(ctx)
    }()

    select {
    case err := <-errCh:
        if err != nil {
            log.Println("failed:", err)
            return
        }
    }

    log.Println("success")
}

func request(ctx context.Context) error {
    tr := &http.Transport{}
    client := &http.Client{Transport: tr}

    req, err := http.NewRequest("POST", backendService, nil)
    if err != nil {
        return err
    }

    // 新たにgoroutineを生成して実際のリクエストを行う
    // 結果はerror channelに投げる
    errCh := make(chan error, 1)
    go func() {
        _, err := client.Do(req)
        errCh <- err
    }()

    select {
    case err := <-errCh:
        if err != nil {
            return err
        }

    // Timeoutが発生する,もしくはCancelが実行されるとChannelが返る
    case <-ctx.Done():
        tr.CancelRequest(req)
        <-errCh
        return ctx.Err()
    }

    return nil
}

valueにはどんな値を格納すべきか?

・map[interface{}]interface{}であり、完全に型安全ではなく、コンパイラでチェックできないということです。
・文字列やデータ構造体のような値を格納するためにコンテキストを使用し、ポインタやハンドルのような参照を格納するためにコンテキストを使用するのは避けたほうが良い
・リクエストのライフサイクルが始まる前に、そこに入れた情報がミドルウェアチェーンで利用可能かどうかも判断基準に

context使用上の注意とまとめ

  • 構造体の中には、Contextを保持してはならない。Contextが必要な関数には明示的に渡す事。また、Contextは第1引数であるべきで、だいたいはctxと名付ける

→contextを含んだpublicな構造体を定義すると、いろんなところからcontexを使えてしまうため、意図しないcancelや親子関係が複雑になってしまうから

  • 関数側が許容するとしても、nilのContextを渡してはいけない。どのコンテキスト渡して良いか確証が持てない時は、Context.TODOを使って空のContextを渡す。

  • 同じContextは別々に実行されているgoroutineで関数渡しても良い。Contextは複数のgoroutineから同時に使われても安全。

  • Context leakを避ける。WithCancelやWithTimeout,WithDeadlineで返されるcancelが呼ばれないと,その親Contextがcancelされるまでその子ContextがLeakする。→go vetで指摘してくれる

  • contextをもつ関数は適切なキャンセル処理を実装するべきである.この関数を使う側は呼び出し側(つまり親context)でTimeoutが発生した,もしくはCancelを実行した場合に適切にキャンセル処理・リソースの解放が実行されることを期待する。

  • ContextのValueはAPIやプロセスをまたぐリクエストスコープな値だけに使う。オプショナルな値を関数に渡すためではない。

  • contextはos,net,net/httpなど他のパッケージでも使われている

0
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
2