序文
多くの Go プロジェクトのソース コードでは、読み取りのプロセスで非常に一般的なパラメーター ctx が見つかります。これは基本的に関数の最初のパラメーターとして使用されます。
なぜこのように書くのですか?このパラメータは何に使われるのでしょうか? そんな疑問を持ちながら、このパラメータの裏話を調べてみました。
始まりの写真:
中心となるのは Context インターフェイスです。
// A Context carries a deadline, cancelation signal, and request-scoped values
// across API boundaries. Its methods are safe for simultaneous use by multiple
// goroutines.
type Context interface {
// Done returns a channel that is closed when this Context is canceled
// or times out.
Done() <-chan struct{
}
// Err indicates why this context was canceled, after the Done channel
// is closed.
Err() error
// Deadline returns the time when this Context will be canceled, if any.
Deadline() (deadline time.Time, ok bool)
// Value returns the value associated with key or nil if none.
Value(key interface{
}) interface{
}
}
次の 4 つのメソッドが含まれています。
- Done(): タイムアウトまたは cancel メソッドが呼び出されたときにチャネルを返します。
- Err(): ctx をキャンセルした理由を示すエラーを返します。
- Deadline(): 期限とブール値を返します。
- Value(): キーに対応する値を返します。
このインターフェイスを実装する 4 つの構造体、つまり emptyCtx、cancelCtx、timerCtx、および valueCtx があります。
このうち、emptyCtx は空の型であり、次の 2 つのメソッドを公開します。
func Background() Context
func TODO() Context
通常の状況では、Background() をルート ctx として使用し、それに基づいて子 ctx を派生します。どの ctx を使用すればよいかわからない場合は、TODO() を使用してください。
他の 3 つも、対応するメソッドを公開します。
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
func WithValue(parent Context, key, val interface{
}) Context
ルールに従う
Context を使用する場合は、次の 4 つのルールに従ってください。
- コンテキストを構造体に入れず、ctx という名前の最初のパラメーターとして渡します。
- 関数で許可されている場合でも、nil Context を渡さないでください。どの Context を使用すればよいかわからない場合は、context.TODO() を使用できます。
- Context を使用する値関連のメソッドは、プログラムやインターフェイスのリクエストに関連するメタデータを渡すためにのみ使用する必要があり、一部のオプションのパラメーターを渡すためには使用しないでください。
- 同じコンテキストを異なるゴルーチンに渡すことができ、コンテキストは同時実行に対して安全です。
ありキャンセル
goCopy コード関数 WithCancel(parent Context) (ctx Context、キャンセル CancelFunc)
WithCancel は、新しい Done チャネルを持つ親のコピーを返します。返された ctx の Done チャネルは、返された cancel 関数が呼び出されたとき、または親コンテキストの Done チャネルが閉じられたときに閉じられます。
このコンテキストをキャンセルすると、それに関連付けられたリソースが解放されるため、コードはこのコンテキストで実行中の操作が完了したらすぐに cancel を呼び出す必要があります。
例として、
このコードはキャンセル可能なコンテキストを使用して goroutine リークを防ぐ方法を示しています。関数の最後に、gen によって開始された goroutine がリークせずに返されます。
package main
import (
"context"
"fmt"
)
func main() {
// gen generates integers in a separate goroutine and
// sends them to the returned channel.
// The callers of gen need to cancel the context once
// they are done consuming generated integers not to leak
// the internal goroutine started by gen.
gen := func(ctx context.Context) <-chan int {
dst := make(chan int)
n := 1
go func() {
for {
select {
case <-ctx.Done():
return // returning not to leak the goroutine
case dst <- n:
n++
}
}
}()
return dst
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel() // cancel when we are finished consuming integers
for n := range gen(ctx) {
fmt.Println(n)
if n == 5 {
break
}
}
}
出力:
2
3
4
5
締め切りあり
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)
WithDeadline は、期限が d 以降になるように調整された親コンテキストのコピーを返します。親の期限がすでに d より早い場合、WithDeadline(parent, d) は意味的に親と同等です。
返されたコンテキストの Done チャネルは、期限が切れたとき、返された cancel 関数が呼び出されたとき、または親コンテキストの Done チャネルが閉じられたときに閉じられます。
このコンテキストをキャンセルすると、それに関連付けられたリソースが解放されるため、コードはこのコンテキストで実行中の操作が完了したらすぐに cancel を呼び出す必要があります。
例:
このコードは、期限付きのコンテキストを渡して、期限に達したらすぐに終了する必要があることをブロック関数に伝えます。
package main
import (
"context"
"fmt"
"time"
)
const shortDuration = 1 * time.Millisecond
func main() {
d := time.Now().Add(shortDuration)
ctx, cancel := context.WithDeadline(context.Background(), d)
// Even though ctx will be expired, it is good practice to call its
// cancellation function in any case. Failure to do so may keep the
// context and its parent alive longer than necessary.
defer cancel()
select {
case <-time.After(1 * time.Second):
fmt.Println("overslept")
case <-ctx.Done():
fmt.Println(ctx.Err())
}
}
出力:
context deadline exceeded
タイムアウトあり
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
WithTimeout は WithDeadline(parent, time.Now().Add(timeout)) を返します。
このコンテキストをキャンセルすると、それに関連付けられたリソースが解放されるため、コードはこのコンテキストで実行中の操作が完了したらすぐに cancel を呼び出す必要があります。
例として、
このコードはタイムアウトを伴うコンテキストを渡し、タイムアウト後に終了する必要があることをブロック関数に伝えます。
package main
import (
"context"
"fmt"
"time"
)
const shortDuration = 1 * time.Millisecond
func main() {
// Pass a context with a timeout to tell a blocking function that it
// should abandon its work after the timeout elapses.
ctx, cancel := context.WithTimeout(context.Background(), shortDuration)
defer cancel()
select {
case <-time.After(1 * time.Second):
fmt.Println("overslept")
case <-ctx.Done():
fmt.Println(ctx.Err()) // prints "context deadline exceeded"
}
}
出力:
context deadline exceeded
値あり
func WithValue(parent Context, key, val any) Context
WithValue は、キーに関連付けられた値が val である親のコピーを返します。
ここで、キーは比較可能である必要があり、コンテキストを使用するパッケージ間の競合を避けるために、文字列型またはその他の組み込み型であってはなりません。WithValue のユーザーは独自のキー タイプを定義する必要があります。
Interface{} への割り当てを避けるために、コンテキスト キーは通常、具体的な struct{} タイプになります。あるいは、エクスポートされたコンテキスト キー変数の静的型はポインターまたはインターフェイスである必要があります。
例として、
このコードはコンテキストに値を渡す方法と、それを取得する方法 (値が存在する場合) を示しています。
package main
import (
"context"
"fmt"
)
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("language")
ctx := context.WithValue(context.Background(), k, "Go")
f(ctx, k)
f(ctx, favContextKey("color"))
}
出力:
ound value: Go
key not found: color
コード例を含むこの記事の内容のほとんどは公式ドキュメントから翻訳されており、コードは検証および実行可能です。特に分かりにくい箇所がある場合は、公式ドキュメントを直接読んでいただいても結構です。