Goでコンテキストを使用する方法
著者は、 Diversity in Tech Fund を選択して、 Write forDOnationsプログラムの一環として寄付を受け取りました。
序章
特にサーバーソフトウェアで大規模なアプリケーションを開発する場合、関数がそれ自体で動作するために必要な情報とは別に、関数が実行されている環境について詳しく知ることが役立つ場合があります。 たとえば、Webサーバー関数が特定のクライアントのHTTP要求を処理している場合、関数は、クライアントが応答を提供するために要求しているURLを知るだけでよい場合があります。 関数は、そのURLのみをパラメーターとして受け取る場合があります。 ただし、応答を受信する前にクライアントが切断するなど、応答を提供するときに常に問題が発生する可能性があります。 応答を提供する関数がクライアントが切断されたことを認識しない場合、サーバーソフトウェアは、使用されない応答の計算に必要な時間よりも多くの計算時間を費やしてしまう可能性があります。
この場合、クライアントの接続ステータスなどの要求のコンテキストを認識することで、クライアントが切断するとサーバーは要求の処理を停止できます。 これにより、ビジー状態のサーバー上の貴重なコンピューティングリソースが節約され、別のクライアントの要求を処理できるように解放されます。 このタイプの情報は、データベース呼び出しなど、関数の実行に時間がかかる他のコンテキストでも役立ちます。 このタイプの情報へのユビキタスアクセスを可能にするために、Goはその標準ライブラリにcontext
パッケージを含めました。
このチュートリアルでは、関数内のコンテキストを使用するGoプログラムを作成することから始めます。 次に、そのプログラムを更新して、コンテキストに追加のデータを格納し、別の関数から取得します。 最後に、コンテキストの機能を使用して、追加データの処理を停止するために完了したことを通知します。
前提条件
- Goバージョン1.16以降がインストールされています。これは、シリーズGoのローカルプログラミング環境をインストールおよびセットアップする方法に従って実行できます。
- チュートリアルGoで複数の関数を同時に実行する方法にあるゴルーチンとチャネルの理解。
- チュートリアルGoでの日付と時刻の使用方法にあるGoでの日付と時刻の使用に精通していること。
switch
ステートメントの経験。これについては、チュートリアルGoでSwitchステートメントを作成する方法を参照してください。
コンテキストの作成
Goの多くの関数は、context
パッケージを使用して、実行されている環境に関する追加情報を収集し、通常、それらが呼び出す関数にそのコンテキストを提供します。 context
パッケージのcontext.Context
インターフェイスを使用し、それを関数間で渡すことにより、プログラムは、main
などのプログラムの最初の関数からその情報を伝達できます。プログラムで最も深い関数呼び出しへの道。 たとえば、http.RequestのContext関数は、リクエストを行っているクライアントに関する情報を含むcontext.Context
を提供し、クライアントが以前に切断した場合に終了します。リクエストは終了しました。 このcontext.Context
値を関数に渡して、sql.DBのQueryContext関数を呼び出すことにより、データベースクエリも停止します。クライアントが切断したときに実行されます。
このセクションでは、コンテキストをパラメーターとして受け取る関数を使用してプログラムを作成します。 また、context.TODO
およびcontext.Background
関数で作成した空のコンテキストを使用してその関数を呼び出します。
プログラムでコンテキストの使用を開始するには、プログラムを保持するためのディレクトリが必要です。 多くの開発者は、プロジェクトを整理するためにディレクトリにプロジェクトを保持しています。 このチュートリアルでは、projects
という名前のディレクトリを使用します。
まず、projects
ディレクトリを作成し、次の場所に移動します。
- mkdir projects
- cd projects
次に、プロジェクトのディレクトリを作成します。 この場合、ディレクトリcontexts
を使用します。
- mkdir contexts
- cd contexts
contexts
ディレクトリ内で、nano
またはお気に入りのエディタを使用して、main.go
ファイルを開きます。
- nano main.go
main.go
ファイルで、context.Context
をパラメーターとして受け入れるdoSomething
関数を作成します。 次に、コンテキストを作成し、そのコンテキストを使用してdoSomething
を呼び出すmain
関数を追加します。
main.go
に次の行を追加します。
package main
import (
"context"
"fmt"
)
func doSomething(ctx context.Context) {
fmt.Println("Doing something!")
}
func main() {
ctx := context.TODO()
doSomething(ctx)
}
main
関数では、context.TODO
関数を使用しました。これは、空の(または開始する)コンテキストを作成する2つの方法のいずれかです。 使用するコンテキストがわからない場合は、これをプレースホルダーとして使用できます。
このコードでは、追加したdoSomething
関数は、context.Context
を唯一のパラメーターとして受け入れますが、まだ何もしていません。 変数の名前はctx
で、これは一般的にコンテキスト値に使用されます。 context.Context
パラメーターを関数の最初のパラメーターとして配置することもお勧めします。これは、Go標準ライブラリに表示されます。 ただし、doSomething
の唯一のパラメーターであるため、これはまだ適用されません。
プログラムを実行するには、main.go
ファイルでgo run
コマンドを使用します。
- go run main.go
出力は次のようになります。
OutputDoing something!
出力は、fmt.Println
関数を使用して関数が呼び出されてDoing something!
を出力したことを示しています。
次に、main.go
ファイルを再度開き、プログラムを更新して、空のコンテキストを作成する2番目の関数context.Background
を使用します。
...
func main() {
ctx := context.Background()
doSomething(ctx)
}
context.Background
関数は、context.TODO
のように空のコンテキストを作成しますが、既知のコンテキストを開始する場合に使用するように設計されています。 基本的に、2つの関数は同じことを行います。つまり、context.Context
として使用できる空のコンテキストを返します。 最大の違いは、他の開発者に意図を伝える方法です。 どちらを使用するかわからない場合は、context.Background
がデフォルトのオプションとして適しています。
次に、go run
コマンドを使用してプログラムを再実行します。
- go run main.go
出力は次のようになります。
OutputDoing something!
コードの機能は変更されておらず、開発者がコードを読んだときに表示されるものだけが変更されたため、出力は同じになります。
このセクションでは、context.TODO
関数とcontext.Background
関数の両方を使用して空のコンテキストを作成しました。 ただし、空のコンテキストがそのままの状態である場合は、完全に役立つわけではありません。 それらを他の関数に渡して使用することもできますが、独自のコードで使用する場合は、これまでのところ、空のコンテキストしかありません。 コンテキストに情報を追加するためにできることの1つは、他の関数でそれらから取得できるデータを追加することです。これについては、次のセクションで行います。
コンテキスト内でのデータの使用
プログラムでcontext.Context
を使用する利点の1つは、コンテキスト内に格納されているデータにアクセスできることです。 コンテキストにデータを追加し、関数から関数にコンテキストを渡すことにより、プログラムの各レイヤーは、何が起こっているかについての追加情報を追加できます。 たとえば、最初の関数はコンテキストにユーザー名を追加する場合があります。 次の関数は、ユーザーがアクセスしようとしているコンテンツにファイルパスを追加する場合があります。 最後に、3番目の関数は、システムのディスクからファイルを読み取り、ファイルが正常にロードされたかどうか、およびどのユーザーがファイルをロードしようとしたかをログに記録できます。
コンテキストに新しい値を追加するには、context
パッケージのcontext.WithValue
関数を使用します。 この関数は、親context.Context
、キー、および値の3つのパラメーターを受け入れます。 親コンテキストは、親コンテキストに関する他のすべての情報を保持しながら、値を追加するコンテキストです。 次に、キーを使用してコンテキストから値を取得します。 キーと値は任意のデータ型にすることができますが、このチュートリアルではstring
キーと値を使用します。 context.WithValue
は、値が追加された新しいcontext.Context
値を返します。
値が追加されたcontext.Context
を取得したら、context.Context
のValue
メソッドを使用してそれらの値にアクセスできます。 Value
メソッドにキーを指定すると、保存されている値が返されます。
ここで、main.go
ファイルを再度開き、context.WithValue
を使用してコンテキストに値を追加するように更新します。 次に、doSomething
関数を更新して、fmt.Printf
を使用してその値を出力に出力します。
...
func doSomething(ctx context.Context) {
fmt.Printf("doSomething: myKey's value is %s\n", ctx.Value("myKey"))
}
func main() {
ctx := context.Background()
ctx = context.WithValue(ctx, "myKey", "myValue")
doSomething(ctx)
}
このコードでは、親コンテキストを保持するために使用されるctx
変数に新しいコンテキストを割り当てています。 これは、特定の親コンテキストを参照する理由がない場合に使用する比較的一般的なパターンです。 親コンテキストにもアクセスする必要がある場合は、後で説明するように、この値を新しい変数に割り当てることができます。
プログラムの出力を確認するには、go run
コマンドを使用してプログラムを実行します。
- go run main.go
出力は次のようになります。
OutputdoSomething: myKey's value is myValue
出力には、main
関数からコンテキストに保存した値がdoSomething
関数でも使用できるようになっていることがわかります。 サーバーで実行されているより大きなプログラムでは、この値は、プログラムの実行が開始された時刻や、プログラムが実行されているサーバーのようなものである可能性があります。
コンテキストを使用する場合、特定のcontext.Context
に格納されている値は不変であり、変更できないことを知っておくことが重要です。 context.WithValue
を呼び出すと、親コンテキストを渡し、コンテキストも返されます。 context.WithValue
関数が指定したコンテキストを変更しなかったため、コンテキストが返されました。 代わりに、親コンテキストを新しい値で別のコンテキスト内にラップしました。
これがどのように機能するかを確認するには、main.go
ファイルを更新して、context.Context
を受け入れ、コンテキストからmyKey
値を出力する新しいdoAnother
関数を追加します。 次に、doSomething
を更新して、コンテキストに独自のmyKey
値(anotherValue
)を設定し、結果のanotherCtx
コンテキストでdoAnother
を呼び出します。 最後に、doSomething
にmyKey
の値を元のコンテキストから再度出力させます。
...
func doSomething(ctx context.Context) {
fmt.Printf("doSomething: myKey's value is %s\n", ctx.Value("myKey"))
anotherCtx := context.WithValue(ctx, "myKey", "anotherValue")
doAnother(anotherCtx)
fmt.Printf("doSomething: myKey's value is %s\n", ctx.Value("myKey"))
}
func doAnother(ctx context.Context) {
fmt.Printf("doAnother: myKey's value is %s\n", ctx.Value("myKey"))
}
...
次に、go run
コマンドを使用してプログラムを再実行します。
- go run main.go
出力は次のようになります。
OutputdoSomething: myKey's value is myValue
doAnother: myKey's value is anotherValue
doSomething: myKey's value is myValue
今回の出力では、doSomething
関数からの2行と、doAnother
関数からの1行が表示されます。 main
関数で、myKey
をmyValue
の値に設定し、それをdoSomething
関数に渡しました。 myValue
が関数に組み込まれたことが出力でわかります。
ただし、次の行は、doSomething
でcontext.WithValue
を使用してmyKey
をanotherValue
に設定し、結果のanotherCtx
コンテキストを渡した場合を示しています。 doAnother
に変更すると、新しい値が初期値を上書きします。
最後に、最後の行で、元のコンテキストからmyKey
値を再度出力すると、値はmyValue
のままであることがわかります。 context.WithValue
関数は親コンテキストのみをラップするため、親コンテキストには元の値と同じ値がすべて含まれます。 コンテキストでValue
メソッドを使用すると、指定されたキーの最も外側にラップされた値が検索され、その値が返されます。 コードでは、myKey
に対してanotherCtx.Value
を呼び出すと、anotherValue
が返されます。これは、コンテキストの最も外側にラップされた値であり、他のmyKey
を実質的にオーバーライドするためです。 ]値がラップされています。 doSomething
内でctx.Value
をもう一度呼び出すと、anotherCtx
はctx
をラップしないため、元のmyValue
値が返されます。
注:コンテキストは、保持できるすべての値を備えた強力なツールですが、コンテキストに格納されているデータとパラメーターとして関数に渡されるデータの間でバランスを取る必要があります。 すべてのデータをコンテキストに入れて、パラメーターの代わりにそのデータを関数で使用したくなるかもしれませんが、それは読み取りと保守が難しいコードにつながる可能性があります。 経験則として、関数の実行に必要なデータはすべてパラメーターとして渡す必要があります。 たとえば、後で情報をログに記録するときに使用するために、ユーザー名などの値をコンテキスト値に保持すると便利な場合があります。 ただし、ユーザー名を使用して関数に特定の情報を表示するかどうかを決定する場合は、コンテキストから既に利用可能であっても、ユーザー名を関数パラメーターとして含める必要があります。 このように、あなたや他の誰かが将来関数を見るとき、どのデータが実際に使用されているかを簡単に確認できます。
このセクションでは、コンテキストに値を格納するようにプログラムを更新し、そのコンテキストをラップして値をオーバーライドしました。 ただし、前の例で述べたように、これは、コンテキストが提供できる唯一の価値のあるツールではありません。 また、不要なリソースの使用を避けるために処理を停止する必要がある場合に、プログラムの他の部分に信号を送るために使用することもできます。
コンテキストの終了
context.Context
が提供するもう1つの強力なツールは、コンテキストが終了し、完了したと見なす必要があることを、それを使用するすべての関数に通知する方法です。 コンテキストが実行されたことをこれらの関数に通知することにより、コンテキストに関連する作業の処理を停止することがわかります。 コンテキストのこの機能を使用すると、すべての関数を完全に完了する代わりに、結果が破棄されたとしても、その処理時間を他の目的に使用できるため、プログラムをより効率的にすることができます。 たとえば、WebページのリクエストがGo Webサーバーに届いた場合、ユーザーはページの読み込みが完了する前に「停止」ボタンを押すか、ブラウザを閉じることになります。 要求されたページでいくつかのデータベースクエリを実行する必要がある場合、データが使用されなくてもサーバーがクエリを実行する可能性があります。 ただし、関数がcontext.Context
を使用している場合、GoのWebサーバーがコンテキストをキャンセルし、まだ実行されていない他のデータベースクエリの実行をスキップできるため、関数はコンテキストが完了したことを認識します。 これにより、Webサーバーとデータベースサーバーの処理時間が解放され、代わりに別のリクエストで使用できるようになります。
このセクションでは、コンテキストが完了したことを通知するようにプログラムを更新し、3つの異なるメソッドを使用してコンテキストを終了します。
コンテキストが完了したかどうかの判断
コンテキストが終了する原因に関係なく、コンテキストが実行されたかどうかの判断は同じ方法で行われます。 context.Context
タイプは、Done
と呼ばれるメソッドを提供します。このメソッドをチェックして、コンテキストが終了したかどうかを確認できます。 このメソッドは、コンテキストが完了すると閉じられるチャネルを返します。このチャネルが閉じられるのを監視している関数は、実行コンテキストが完了したと見なし、コンテキストに関連する処理を停止する必要があることを認識します。 Done
メソッドは、そのチャネルに値が書き込まれることがないため機能します。チャネルが閉じられると、そのチャネルは読み取りの試行ごとにnil
値を返し始めます。 Done
チャネルが閉じているかどうかを定期的にチェックし、その間に処理作業を行うことで、作業はできるが、処理を早期に停止する必要があるかどうかもわかる関数を実装できます。 この処理作業を組み合わせることで、Done
チャネルとselect
ステートメントの定期的なチェックがさらに進み、他のチャネルとの間でデータを同時に送受信できるようになります。
Goのselectステートメントは、プログラムが同時に複数のチャネルからの読み取りまたは複数のチャネルへの書き込みを試行できるようにするために使用されます。 select
ステートメントごとに1つのチャネル操作のみが発生しますが、ループで実行される場合、プログラムは1つが使用可能になったときに複数のチャネル操作を実行できます。 select
ステートメントは、キーワードselect
と、それに続く1つ以上のcase
ステートメントを含む中括弧({}
)で囲まれたコードブロックを使用して作成されます。コードブロック内。 各case
ステートメントは、チャネルの読み取りまたは書き込み操作のいずれかであり、select
ステートメントは、case
ステートメントの1つが実行されるまでブロックされます。 ただし、select
ステートメントをブロックしたくないとします。 その場合、他のcase
ステートメントを実行できない場合にすぐに実行される、default
ステートメントを追加することもできます。 外観と動作はswitch
ステートメントと似ていますが、チャネル用です。
次のコード例は、select
ステートメントを、チャネルから結果を受信するだけでなく、コンテキストのDone
チャネルが閉じられるタイミングを監視する長時間実行関数で使用できる可能性があることを示しています。
ctx := context.Background()
resultsCh := make(chan *WorkResult)
for {
select {
case <- ctx.Done():
// The context is over, stop processing results
return
case result := <- resultsCh:
// Process the results received
}
}
このコードでは、ctx
とresultsCh
の値は通常、パラメーターとして関数に渡され、ctx
は関数が実行されているcontext.Context
です。 resultsCh
は、他の場所のワーカーゴルーチンからの結果の読み取り専用チャネルです。 select
ステートメントが実行されるたびに、Goは関数の実行を停止し、すべてのcase
ステートメントを監視します。 case
ステートメントのいずれかを実行できる場合、resultsCh
の場合はチャネルからの読み取り、チャネルへの書き込み、Done
チャネル、select
ステートメントのそのブランチが実行されます。 ただし、case
ステートメントが複数同時に実行できる場合、どの順序で実行されるかは保証されません。
この例のコード実行では、return
ステートメントのみがそのcase
ステートメント内にあるため、for
ループはctx.Done
チャネルが閉じられるまで永久に続きます。 。 case <- ctx.Done
はどの変数にも値を割り当てませんが、チャネルには無視されても読み取ることができる値があるため、ctx.Done
が閉じられたときにトリガーされます。 ctx.Done
チャネルが閉じられていない場合、select
ステートメントは閉じられるまで、またはresultsCh
に読み取り可能な値がある場合に待機します。 resultsCh
を読み取ることができる場合は、代わりにそのcase
ステートメントのコードブロックが実行されます。 順序は保証されていないため、両方を読み取ることができれば、どちらが実行されるかはランダムに見えます。
例のselect
ステートメントにコードが含まれていないdefault
ブランチがある場合、コードの動作は実際には変更されず、select
ステートメントが終了するだけです。すぐに、for
ループは、select
ステートメントの別の反復を開始します。 これにより、for
ループが非常に高速に実行されます。これは、ループが停止してチャネルからの読み取りを待機することがないためです。 これが発生した場合、for
ループはビジーループと呼ばれます。これは、何かが発生するのを待つ代わりに、ループが何度も何度も実行されているためです。 プログラムが他のコードを実行させるために実行を停止する機会がないため、これは多くのCPUを消費する可能性があります。 ただし、この機能が役立つ場合もあります。たとえば、チャネルが何かを実行する準備ができているかどうかを確認してから、別の非チャネル操作を実行する場合などです。
サンプルコードのfor
ループを終了する唯一の方法は、Done
によって返されたチャネルを閉じることであり、Done
チャネルを閉じる唯一の方法は、コンテキストでは、コンテキストを終了する方法が必要になります。 Go context
パッケージは、目標に応じてこれを行うためのいくつかの方法を提供します。最も直接的なオプションは、関数を呼び出してコンテキストを「キャンセル」することです。
コンテキストのキャンセル
コンテキストをキャンセルすることは、コンテキストを終了するための最も簡単で制御可能な方法です。 context.WithValue
のコンテキストに値を含めるのと同様に、context.WithCancel
関数を使用して「キャンセル」関数をコンテキストに関連付けることができます。 この関数は、親コンテキストをパラメーターとして受け取り、新しいコンテキストと、返されたコンテキストをキャンセルするために使用できる関数を返します。 また、context.WithValue
と同様に、返されたキャンセル関数を呼び出すと、返されたコンテキストと、それを親コンテキストとして使用するコンテキストのみがキャンセルされます。 これは、親コンテキストのキャンセルを妨げるものではなく、独自のキャンセル関数を呼び出してもキャンセルされないことを意味します。
次に、main.go
ファイルを開き、context.WithCancel
とキャンセル機能を使用するようにプログラムを更新します。
package main
import (
"context"
"fmt"
"time"
)
func doSomething(ctx context.Context) {
ctx, cancelCtx := context.WithCancel(ctx)
printCh := make(chan int)
go doAnother(ctx, printCh)
for num := 1; num <= 3; num++ {
printCh <- num
}
cancelCtx()
time.Sleep(100 * time.Millisecond)
fmt.Printf("doSomething: finished\n")
}
func doAnother(ctx context.Context, printCh <-chan int) {
for {
select {
case <-ctx.Done():
if err := ctx.Err(); err != nil {
fmt.Printf("doAnother err: %s\n", err)
}
fmt.Printf("doAnother: finished\n")
return
case num := <-printCh:
fmt.Printf("doAnother: %d\n", num)
}
}
}
...
まず、time
パッケージのインポートを追加し、doAnother
関数を変更して、画面に印刷する新しい数値チャネルを受け入れます。 次に、for
ループ内でselect
ステートメントを使用して、そのチャネルとコンテキストのDone
メソッドから読み取ります。 次に、doSomething
関数で、キャンセルできるコンテキストと番号を送信するチャネルを作成し、それらをパラメーターとしてdoAnother
をゴルーチンとして実行します。 最後に、チャネルにいくつかの番号を送信し、コンテキストをキャンセルします。
このコードが実行されていることを確認するには、以前と同じようにgo run
コマンドを使用します。
- go run main.go
出力は次のようになります。
OutputdoAnother: 1
doAnother: 2
doAnother: 3
doAnother err: context canceled
doAnother: finished
doSomething: finished
この更新されたコードでは、doSomething
関数は、作業チャネルから読み取った1つ以上のgoroutinesに作業を送信する関数のように機能します。 この場合、doAnother
がワーカーであり、数値の印刷がその作業です。 doAnother
ゴルーチンが開始されると、doSomething
は印刷する番号の送信を開始します。 doAnother
関数内で、select
ステートメントは、ctx.Done
チャネルが閉じるか、printCh
チャネルで番号が受信されるのを待機しています。 doSomething
関数は、チャネル上で3つの番号を送信し、番号ごとにfmt.Printf
をトリガーしてから、cancelCtx
関数を呼び出してコンテキストをキャンセルします。 doAnother
関数は、チャネルから3つの数値を読み取った後、次のチャネル操作を待機します。 次に発生するのはdoSomething
がcancelCtx
を呼び出すことなので、ctx.Done
ブランチが呼び出されます。
ctx.Done
ブランチが呼び出されると、コードはcontext.Context
によって提供されるErr
関数を使用して、コンテキストがどのように終了したかを判別します。 プログラムはcancelCtx
関数を使用してコンテキストをキャンセルしているため、出力に表示されるエラーはcontext canceled
です。
注:以前にGoプログラムを実行してログ出力を確認したことがある場合は、過去にcontext canceled
エラーが発生した可能性があります。 Go http パッケージを使用する場合、これは、サーバーが完全な応答を処理する前にクライアントがサーバーから切断されたときに表示される一般的なエラーです。
doSomething
関数がコンテキストをキャンセルすると、time.Sleep
関数を使用して短時間待機し、doAnother
にキャンセルされたコンテキストを処理して、実行を終了する時間を与えます。 その後、関数を終了します。 多くの場合、time.Sleep
は必要ありませんが、サンプルコードの実行が非常に速く完了するため、必要になります。 time.Sleep
が含まれていない場合、プログラムの残りの出力が画面に表示される前にプログラムが終了することがあります。
context.WithCancel
関数とそれが返すキャンセル関数は、コンテキストがいつ終了するかを正確に制御したい場合に最も役立ちますが、その量の制御が必要ない場合もあります。 context
パッケージのコンテキストを終了するために使用できる次の関数はcontext.WithDeadline
であり、コンテキストを自動的に終了する最初の関数です。
コンテキストに期限を与える
コンテキストでcontext.WithDeadline
を使用すると、コンテキストを終了する必要がある場合の期限を設定でき、その期限が過ぎると自動的に終了します。 コンテキストの期限を設定することは、自分で期限を設定することに似ています。 コンテキストに終了する必要がある時間を通知すると、その時間を超えると、Goによってコンテキストが自動的にキャンセルされます。
コンテキストに期限を設定するには、context.WithDeadline
関数を使用して、親コンテキストと、コンテキストをキャンセルするタイミングのtime.Time
値を指定します。 次に、新しいコンテキストと、新しいコンテキストをキャンセルする関数を戻り値として受け取ります。 context.WithCancel
と同様に、期限を超えると、新しいコンテキストと、それを親コンテキストとして使用する他のコンテキストにのみ影響します。 context.WithCancel
の場合と同じようにキャンセル関数を呼び出すことにより、コンテキストを手動でキャンセルすることもできます。
次に、main.go
ファイルを開き、context.WithCancel
の代わりにcontext.WithDeadline
を使用するように更新します。
...
func doSomething(ctx context.Context) {
deadline := time.Now().Add(1500 * time.Millisecond)
ctx, cancelCtx := context.WithDeadline(ctx, deadline)
defer cancelCtx()
printCh := make(chan int)
go doAnother(ctx, printCh)
for num := 1; num <= 3; num++ {
select {
case printCh <- num:
time.Sleep(1 * time.Second)
case <-ctx.Done():
break
}
}
cancelCtx()
time.Sleep(100 * time.Millisecond)
fmt.Printf("doSomething: finished\n")
}
...
更新されたコードは、doSomething
のcontext.WithDeadline
を使用して、time.Now
関数を使用して関数が開始してから1500ミリ秒(1.5秒)後にコンテキストを自動的にキャンセルします。 コンテキストの終了方法を更新することに加えて、他のいくつかの変更が行われました。 cancelCtx
を直接呼び出すか、期限を過ぎて自動キャンセルすることでコードを終了できる可能性があるため、doSomething
関数が更新され、select
ステートメントを使用して番号を送信するようになりました。チャネル。 このように、printCh
(この場合はdoAnother
)から読み取っているものがチャネルから読み取られておらず、ctx.Done
チャネルが閉じている場合、
また、cancelCtx
が2回呼び出されていることにも気付くでしょう。1回目は新しいdefer
ステートメントを介して、もう1回は以前の場所で呼び出されます。 defer cancelCtx()
は、他の呼び出しが常に実行されるため、必ずしも必要ではありませんが、将来return
ステートメントがあり、それが失われる場合に備えて、保持しておくと便利です。 。 コンテキストが期限からキャンセルされた場合でも、使用されたリソースをクリーンアップするためにキャンセル関数を呼び出す必要があるため、これはより安全な手段です。
次に、go run
を使用してプログラムを再度実行します。
- go run main.go
出力は次のようになります。
OutputdoAnother: 1
doAnother: 2
doAnother err: context deadline exceeded
doAnother: finished
doSomething: finished
今回の出力では、3つの数値すべてが出力される前に、context deadline exceeded
エラーのためにコンテキストがキャンセルされたことがわかります。 doSomething
機能の実行開始から1.5秒後に期限が設定され、doSomething
は各番号の送信後1秒待機するように設定されているため、3番目の番号が印刷される前に期限を超えます。 期限を超えると、doAnother
とdoSomething
の両方の機能が実行を終了します。これは、両方ともctx.Done
チャネルが閉じるのを監視しているためです。 time.Now
に追加される時間を調整して、さまざまな期限が出力にどのように影響するかを確認することもできます。 期限が3秒以上になると、期限を超えていないため、エラーがcontext canceled
エラーに戻ることもあります。
context deadline exceeded
エラーは、Goアプリケーションを使用したことがあるか、過去にそれらのログを確認したことがある場合にもよく知られている可能性があります。 このエラーは、クライアントへの応答の送信を完了するのに時間がかかるWebサーバーでよく見られます。 データベースクエリまたは一部の処理に時間がかかる場合、サーバーで許可されているよりもWeb要求に時間がかかる可能性があります。 リクエストが制限を超えると、リクエストのコンテキストがキャンセルされ、context deadline exceeded
エラーメッセージが表示されます。
context.WithCancel
の代わりにcontext.WithDeadline
を使用してコンテキストを終了すると、コンテキストを終了する必要がある特定の時間を指定できます。その時間を自分で追跡する必要はありません。 コンテキストを終了するタイミングがtime.Time
であることがわかっている場合は、context.WithDeadline
がコンテキストのエンドポイントを管理するのに適している可能性があります。 また、コンテキストが終了する特定の時間を気にせず、コンテキストが開始されてから1分後に終了することを知っている場合もあります。 context.WithDeadline
およびその他のtime
パッケージ関数とメソッドを使用してこれを行うことは可能ですが、Goにはcontext.WithTimeout
関数も用意されています。
コンテキストに時間制限を与える
context.WithTimeout
関数は、context.WithDeadline
の周りでより役立つ関数とほとんど見なすことができます。 context.WithDeadline
を使用すると、コンテキストを終了するための特定のtime.Time
を指定できますが、context.WithTimeout
関数を使用すると、time.Duration
の値を指定するだけで済みます。コンテキストを持続させたい。 多くの場合、これが探しているものですが、time.Time
を指定する必要がある場合は、context.WithDeadline
を使用できます。 context.WithTimeout
がないと、time.Now()
とtime.Time
のAdd
メソッドを独自に使用して、特定の時間を終了する必要があります。
最後にもう一度、main.go
ファイルを開き、context.WithDeadline
の代わりにcontext.WithTimeout
を使用するように更新します。
...
func doSomething(ctx context.Context) {
ctx, cancelCtx := context.WithTimeout(ctx, 1500*time.Millisecond)
defer cancelCtx()
...
}
...
ファイルを更新して保存したら、go run
を使用して実行します。
- go run main.go
出力は次のようになります。
OutputdoAnother: 1
doAnother: 2
doAnother err: context deadline exceeded
doAnother: finished
doSomething: finished
今回プログラムを実行すると、context.WithDeadline
を使用したときと同じ出力が表示されます。 エラーメッセージも同じで、context.WithTimeout
は実際にはtime.Now
の計算を行うための単なるラッパーであることを示しています。
このセクションでは、context.Context
を終了する3つの異なる方法を使用するようにプログラムを更新しました。 最初のcontext.WithCancel
では、関数を呼び出してコンテキストをキャンセルできました。 次に、context.WithDeadline
とtime.Time
の値を使用して、特定の時間にコンテキストを自動的に終了しました。 最後に、context.WithTimeout
とtime.Duration
の値を使用して、一定の時間が経過した後にコンテキストを自動的に終了しました。 これらの機能を使用すると、プログラムがコンピューターで必要以上のリソースを消費しないようにすることができます。 それらがコンテキストを返す原因となるエラーを理解すると、独自のGoプログラムや他のGoプログラムでのエラーのトラブルシューティングも容易になります。
結論
このチュートリアルでは、Goのcontext
パッケージをさまざまな方法で操作するプログラムを作成しました。 最初に、context.Context
値をパラメーターとして受け入れる関数を作成し、context.TODO
およびcontext.Background
関数を使用して空のコンテキストを作成しました。 その後、context.WithValue
を使用して新しいコンテキストに値を追加し、Value
メソッドを使用して他の関数で値を取得しました。 最後に、コンテキストのDone
メソッドを使用して、コンテキストの実行がいつ完了したかを判別しました。 関数context.WithCancel
、context.WithDeadline
、およびcontext.WithTimeout
と組み合わせると、独自のコンテキスト管理を実装して、これらのコンテキストを使用するコードの実行時間に制限を設定します。
より多くの例でコンテキストがどのように機能するかについて詳しく知りたい場合は、Go contextパッケージのドキュメントに追加情報が含まれています。
このチュートリアルは、 DigitalOcean How to Code inGoシリーズの一部でもあります。 このシリーズでは、Goの初めてのインストールから、言語自体の使用方法まで、Goに関する多くのトピックを取り上げています。