Go言語

【Go言語】Contextを理解したいんじゃ!!

コードを読んでいると頻繁に遭遇する Context について整理したいと思います。

(恥ずかしながら今まで雰囲気で読んでいました)

長々書いていますが、次の3つから得られる情報ばかりです。

改訂2版 みんなのGo言語は現場でGo言語使うときに役立つTipsや知っておくと良いことがまとまっており、Go言語による並行処理は並行処理を書くときのパターンを学ぶことができます。

どちらもGo言語を学ぶ上で役立つことばかりなのでオススメです!!

 

Contextとは?

なんのためにContextが必要なのか?

Go Blog 「Go Concurrency Patterns: Context」のイントロダクションがわかりやすかったので引用します。

In Go servers, each incoming request is handled in its own goroutine. Request handlers often start additional goroutines to access backends such as databases and RPC services. The set of goroutines working on a request typically needs access to request-specific values such as the identity of the end user, authorization tokens, and the request's deadline. When a request is canceled or times out, all the goroutines working on that request should exit quickly so the system can reclaim any resources they are using.

At Google, we developed a context package that makes it easy to pass request-scoped values, cancelation signals, and deadlines across API boundaries to all the goroutines involved in handling a request. The package is publicly available as context. This article describes how to use the package and provides a complete working example.

引用: https://blog.golang.org/context

下手くそなりに訳すと、

Goサーバーでは、各リクエストは個別のゴルーチンで処理されます。リクエストハンドラーはDBやRPCなどにアクセスするために追加のゴルーチンを開始することがよくあります。

リクエストに対して動作する一連のゴルーチンは、たいていユーザーIDや認証トークン、リクエストの期限(デッドライン)などのリクエスト固有の値にアクセスする必要があり、リクエストがキャンセル、タイムアウトしたときには、システムがリソースを使えるように、すべてのゴルーチンはすぐに終了すべきです。

Googleでは、リクエストスコープの値やキャンセル通知、API間の期限(デッドライン)を簡単に伝達できる`context`パッケージを開発しました。

要は、

ポイント

キャンセル通知や期限(デッドライン)、その他のリクエストスコープの値を伝達する手段が欲しく、context パッケージが作られた

Package context のドキュメント Overview を読んで雰囲気を掴む

なんとなく、Contextがある意味がわかったところで、contextパッケージのドキュメント を読んでみます。

Overviewには次のように書かれています。

Package context defines the Context type, which carries deadlines, cancellation signals, and other request-scoped values across API boundaries and between processes.

Incoming requests to a server should create a Context, and outgoing calls to servers should accept a Context. The chain of function calls between them must propagate the Context, optionally replacing it with a derived Context created using WithCancel, WithDeadline, WithTimeout, or WithValue. When a Context is canceled, all Contexts derived from it are also canceled.

The WithCancel, WithDeadline, and WithTimeout functions take a Context (the parent) and return a derived Context (the child) and a CancelFunc. Calling the CancelFunc cancels the child and its children, removes the parent's reference to the child, and stops any associated timers.

(省略)

Do not store Contexts inside a struct type; instead, pass a Context explicitly to each function that needs it. The Context should be the first parameter, typically named ctx:

func DoSomething(ctx context.Context, arg Arg) error {
// ... use ctx ...
}

(省略)

引用: https://golang.org/pkg/context/

こちらも下手くそなりに訳すと、

context package は Context 型を定義しており、Context型はAPIの境界を超えてプロセス間で期限やキャンセル通知、その他のリクエストスコープ値を伝達するものです。

サーバーにリクエストが来たらContextを生成するべきで、サーバーへの発信呼び出し(レスポンス?)はContextを受け入れるべきです。これらの間(リクエスト〜レスポンス発信)の関数呼び出しでは、Contextは伝播されるべきです。このとき WithCancelWithDeadlineWithTimeoutWithValueを使用して作成された子Contextに置き換えても良いでしょう。Contextがキャンセルされると、派生したすべての子Contextもキャンセルされます。

WithCancel, WithDeadline, WithTimeout関数は、親Contextを受け取って子ContextとCancelFuncを返す。CancelFuncを呼び出すと、子Contextとさらにその子Contextがキャンセルされ、親Contextから子Contextの参照は取り除かれ、すべての関連するタイマーもストップする。CancelFuncの呼び出しを失敗すると、 親がキャンセルされるかタイマーが発火されるまで、その子と子孫がリークすることになる。

Contextを構造体の中に格納するべきでない。Contextを必要としている関数に明示的に渡すべきで、慣例的に ctx という名前で関数の第一引数にする。

要は、

ポイント

  • Context型はAPIの境界を超えてプロセス間で期限やキャンセル通知、その他のリクエストスコープ値を伝達するもの。
  • リクエスト着信〜レスポンス発信間の関数呼び出しでは、Contextを伝播していくべき。このとき必要に応じて、 WithCancelWithDeadlineWithTimeoutWithValueを使用して子Contextを派生していく。
  • 親Contextがキャンセルされると派生したすべての子Contextもキャンセルされる。
  • 関数の第一引数に ctx という名前で渡して伝播する。

【まとめ】Contextとは?

ここまでのまとめ。

【まとめ】Contextとは?

  • キャンセル通知や期限(デッドライン)、その他のリクエストスコープの値を伝達する手段
  • Contextの伝播は、第一引数に ctx という名前で渡す
  • 必要に応じて WithCancelWithDeadlineWithTimeoutWithValueを使用して子Context(派生)を作成する
  • 親Contextがキャンセルされると派生したすべての子Contextもキャンセルされる

contextパッケージが提供する型・関数

Contextの概要を掴んだので、更にcontextパッケージのドキュメント を読んでパッケージが提供してくれる型・関数を理解していきます。

type Context interface

まずは Context インターフェースから。

コードは次のようになっています。(ざっと和訳していますが、一部省略)

type Context interface {
    // Deadline は処理をキャンセルする時間を返す。
    // 期限を設定していない場合は ok == false を返す。
    Deadline() (deadline time.Time, ok bool)

    // Done は処理をキャンセルする必要がある場合(Contextがキャンセルされたとき)に閉じられたチャネルを返す。
    // Done は Context をキャンセル出来ないときに nil を返す可能性がある。
    // Doneチャネルのクローズは、cancel function が実行された後に非同期に起こる可能性がある。
    // Done は select文で使われるために提供されている。
    //  func Stream(ctx context.Context, out chan<- Value) error {
    //  	for {
    //  		v, err := DoSomething(ctx)
    //  		if err != nil {
    //  			return err
    //  		}
    //  		select {
    //  		case <-ctx.Done():
    //  			return ctx.Err()
    //  		case out <- v:
    //  		}
    //  	}
    //  }
    Done() <-chan struct{}

    // Err は Done がまだ閉じられていない場合 nil を返す。
    // Doneが閉じられている場合は Contextがキャンセルされた or 期限を超えた理由がわかるエラーを返す。
    Err() error

    // Value はこのContextに関連したキーに対応する値を返す。もしキーに対応する値がなければ nil を返す。
    // Context values は プロセスとAPI境界を通過するクエストスコープのデータのみに使用すべきで
    // オプションのパラメーターを渡すために使用するべきではない。
    //
    // キーは Context内の値を特定するもので、 Context に値を格納したい関数は
    // グローバル変数にキーを割り当て、そのキーは context.WithValue と Context.Value の引数に使う。A key can be any type that supports equality;
    // キーには等価比較できる型であれば何でも使える。
    Value(key interface{}) interface{}
}

 

要は、Context インターフェースには Deadline, Done, Err, Value の4つの関数があり

ポイント

  • Deadline は期限を返す
  • Done はチャネルを返す。このチャネルはキャンセル・期限切れの場合に閉じられる
  • ErrDone チャネルが閉じられた理由を返す
  • ValueContext に格納した値を返す。

提供されている関数たち

提供関数

  • Background
  • WithCancel
  • WithDeadline
  • WithTimeout
  • WithValue

Background

func Background() Context

Contextの生成に使われ、空のContextを返します。

一般的に、メイン関数や初期化などでトップレベルのContextを生成するために使います。

ctx := context.Background()

 

WithCancel

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

新しい Done チャネルをもった子Context(親Contextのコピー)と CancelFunc を返します。この子Contextの Done チャネルが閉じられるのは、 CancelFunc が呼び出されたときか、親Contextの Done チャネルが閉じられたときです。

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

 

WithDeadline, WithTimeout

func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)

d 以内を期限とした子Context(親Contextのコピー)とCancelFunc を返します。もし親Contextの期限が d よりも早ければ親Contextと同じになります。

返された子ContextのDone チャネルが閉じられるのは、「期限を過ぎた場合」、「CancelFunc が呼び出された場合」、「親Contextの Done チャネルが閉じられた場合」の3パターン。

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

WithTimeoutWithDeadline のエイリアスみたいなもので WithDeadline(parent, time.Now().Add(timeout)) と同じ。

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

 

WithValue

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

キーに対応する値をもった子Context(親Contextのコピー)を返す。

ctx = context.WithValue(context.Background(), "userID", 1)

 

Contextを使ったサンプルコード

簡単なサンプルコード

package main

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

func main() {
	// contextを2つ生成
	ctx1, cancel1 := context.WithCancel(context.Background())
	ctx2, _ := context.WithTimeout(context.Background(), 3*time.Second)

	var wg sync.WaitGroup
	wg.Add(1)
	go func() {
		defer wg.Done()
		childProcess(ctx1, "プロセス1")
	}()

	wg.Add(1)
	go func() {
		defer wg.Done()
		childProcess(ctx2, "プロセス2")
	}()

	// ctx2は3秒でタイムアウトするので、スリープ中にキャンセルされる
	time.Sleep(5 * time.Second)
	// 明示的にcancelFuncを呼び出して、ctx1をキャンセルする
	cancel1()
	wg.Wait()
}

func childProcess(ctx context.Context, name string) {
	for {
		select {
		case <-ctx.Done():
			// キャンセル時の処理
			fmt.Printf("%s canceled. error: %s\n", name, ctx.Err())
			return
		case <-time.After(1 * time.Second):
			fmt.Printf("%s processing...\n", name)
		}
	}
}

これを実行すると次のようになり、タイムアウトとキャンセルが行われているのがわかります。

$ go run main.go
プロセス1 processing...
プロセス2 processing...
プロセス2 processing...
プロセス1 processing...
プロセス2 canceled. error: context deadline exceeded
プロセス1 processing...
プロセス1 processing...
プロセス1 canceled. error: context canceled

 

参考

長々書きましたが、次の3つから学ぶことができます。

改訂2版 みんなのGo言語は現場でGo言語使うときに役立つTips集、知っておきたいことがまとまっており、Go言語による並行処理は並行処理を書くときのパターンを学ぶことができます。

どちらもGo言語を学ぶ上で役立つことばかりなのでオススメです!!

イチオシ記事

1

自己紹介 フリーランスエンジニアをしているヨノと申します。 独学でプログラミングを学び、ソシャゲ・SaaS開発などを経て、2018年からフリーランスエンジニアとして活動しています。 主にバックエンド中 ...

2

はじめまして、フリーランスエンジニアのヨノと申します。 自己紹介 独学でプログラミングを学び、ソシャゲ・SaaS開発などを経て、2018年からフリーランスエンジニアとして活動しています。 主にバックエ ...

3

ネット上で色々言われているフリーランスエンジニア....。「本当はどうなの?」と思っている人は多いでしょう。 そこで本記事ではフリーランスエンジニア5年生の私が、ネット上の意見も引用しながら実態を解説 ...

-Go言語