著者は、 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.RequestContext関数は、リクエストを行っているクライアントに関する情報を含むcontext.Contextを提供し、クライアントが以前に切断した場合に終了します。リクエストは終了しました。 このcontext.Context値を関数に渡して、sql.DBQueryContext関数を呼び出すことにより、データベースクエリも停止します。クライアントが切断したときに実行されます。

このセクションでは、コンテキストをパラメーターとして受け取る関数を使用してプログラムを作成します。 また、context.TODOおよびcontext.Background関数で作成した空のコンテキストを使用してその関数を呼び出します。

プログラムでコンテキストの使用を開始するには、プログラムを保持するためのディレクトリが必要です。 多くの開発者は、プロジェクトを整理するためにディレクトリにプロジェクトを保持しています。 このチュートリアルでは、projectsという名前のディレクトリを使用します。

まず、projectsディレクトリを作成し、次の場所に移動します。

  1. mkdir projects
  2. cd projects

次に、プロジェクトのディレクトリを作成します。 この場合、ディレクトリcontextsを使用します。

  1. mkdir contexts
  2. cd contexts

contextsディレクトリ内で、nanoまたはお気に入りのエディタを使用して、main.goファイルを開きます。

  1. nano main.go

main.goファイルで、context.Contextをパラメーターとして受け入れるdoSomething関数を作成します。 次に、コンテキストを作成し、そのコンテキストを使用してdoSomethingを呼び出すmain関数を追加します。

main.goに次の行を追加します。

projects / contexts / 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コマンドを使用します。

  1. go run main.go

出力は次のようになります。

Output
Doing something!

出力は、fmt.Println関数を使用して関数が呼び出されてDoing something!を出力したことを示しています。

次に、main.goファイルを再度開き、プログラムを更新して、空のコンテキストを作成する2番目の関数context.Backgroundを使用します。

projects / contexts / main.go
...

func main() {
	ctx := context.Background()
	doSomething(ctx)
}

context.Background関数は、context.TODOのように空のコンテキストを作成しますが、既知のコンテキストを開始する場合に使用するように設計されています。 基本的に、2つの関数は同じことを行います。つまり、context.Contextとして使用できる空のコンテキストを返します。 最大の違いは、他の開発者に意図を伝える方法です。 どちらを使用するかわからない場合は、context.Backgroundがデフォルトのオプションとして適しています。

次に、go runコマンドを使用してプログラムを再実行します。

  1. go run main.go

出力は次のようになります。

Output
Doing something!

コードの機能は変更されておらず、開発者がコードを読んだときに表示されるものだけが変更されたため、出力は同じになります。

このセクションでは、context.TODO関数とcontext.Background関数の両方を使用して空のコンテキストを作成しました。 ただし、空のコンテキストがそのままの状態である場合は、完全に役立つわけではありません。 それらを他の関数に渡して使用することもできますが、独自のコードで使用する場合は、これまでのところ、空のコンテキストしかありません。 コンテキストに情報を追加するためにできることの1つは、他の関数でそれらから取得できるデータを追加することです。これについては、次のセクションで行います。

コンテキスト内でのデータの使用

プログラムでcontext.Contextを使用する利点の1つは、コンテキスト内に格納されているデータにアクセスできることです。 コンテキストにデータを追加し、関数から関数にコンテキストを渡すことにより、プログラムの各レイヤーは、何が起こっているかについての追加情報を追加できます。 たとえば、最初の関数はコンテキストにユーザー名を追加する場合があります。 次の関数は、ユーザーがアクセスしようとしているコンテンツにファイルパスを追加する場合があります。 最後に、3番目の関数は、システムのディスクからファイルを読み取り、ファイルが正常にロードされたかどうか、およびどのユーザーがファイルをロードしようとしたかをログに記録できます。

コンテキストに新しい値を追加するには、contextパッケージのcontext.WithValue関数を使用します。 この関数は、親context.Context、キー、および値の3つのパラメーターを受け入れます。 親コンテキストは、親コンテキストに関する他のすべての情報を保持しながら、値を追加するコンテキストです。 次に、キーを使用してコンテキストから値を取得します。 キーと値は任意のデータ型にすることができますが、このチュートリアルではstringキーと値を使用します。 context.WithValueは、値が追加された新しいcontext.Context値を返します。

値が追加されたcontext.Contextを取得したら、context.ContextValueメソッドを使用してそれらの値にアクセスできます。 Valueメソッドにキーを指定すると、保存されている値が返されます。

ここで、main.goファイルを再度開き、context.WithValueを使用してコンテキストに値を追加するように更新します。 次に、doSomething関数を更新して、fmt.Printfを使用してその値を出力に出力します。

projects / contexts / main.go
...

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コマンドを使用してプログラムを実行します。

  1. go run main.go

出力は次のようになります。

Output
doSomething: myKey's value is myValue

出力には、main関数からコンテキストに保存した値がdoSomething関数でも使用できるようになっていることがわかります。 サーバーで実行されているより大きなプログラムでは、この値は、プログラムの実行が開始された時刻や、プログラムが実行されているサーバーのようなものである可能性があります。

コンテキストを使用する場合、特定のcontext.Contextに格納されている値は不変であり、変更できないことを知っておくことが重要です。 context.WithValueを呼び出すと、親コンテキストを渡し、コンテキストも返されます。 context.WithValue関数が指定したコンテキストを変更しなかったため、コンテキストが返されました。 代わりに、親コンテキストを新しい値で別のコンテキスト内にラップしました。

これがどのように機能するかを確認するには、main.goファイルを更新して、context.Contextを受け入れ、コンテキストからmyKey値を出力する新しいdoAnother関数を追加します。 次に、doSomethingを更新して、コンテキストに独自のmyKey値(anotherValue)を設定し、結果のanotherCtxコンテキストでdoAnotherを呼び出します。 最後に、doSomethingmyKeyの値を元のコンテキストから再度出力させます。

projects / contexts / main.go
...

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コマンドを使用してプログラムを再実行します。

  1. go run main.go

出力は次のようになります。

Output
doSomething: myKey's value is myValue doAnother: myKey's value is anotherValue doSomething: myKey's value is myValue

今回の出力では、doSomething関数からの2行と、doAnother関数からの1行が表示されます。 main関数で、myKeymyValueの値に設定し、それをdoSomething関数に渡しました。 myValueが関数に組み込まれたことが出力でわかります。

ただし、次の行は、doSomethingcontext.WithValueを使用してmyKeyanotherValueに設定し、結果のanotherCtxコンテキストを渡した場合を示しています。 doAnotherに変更すると、新しい値が初期値を上書きします。

最後に、最後の行で、元のコンテキストからmyKey値を再度出力すると、値はmyValueのままであることがわかります。 context.WithValue関数は親コンテキストのみをラップするため、親コンテキストには元の値と同じ値がすべて含まれます。 コンテキストでValueメソッドを使用すると、指定されたキーの最も外側にラップされた値が検索され、その値が返されます。 コードでは、myKeyに対してanotherCtx.Valueを呼び出すと、anotherValueが返されます。これは、コンテキストの最も外側にラップされた値であり、他のmyKeyを実質的にオーバーライドするためです。 ]値がラップされています。 doSomething内でctx.Valueをもう一度呼び出すと、anotherCtxctxをラップしないため、元の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
	}
}

このコードでは、ctxresultsChの値は通常、パラメーターとして関数に渡され、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とキャンセル機能を使用するようにプログラムを更新します。

projects / contexts / main.go
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コマンドを使用します。

  1. go run main.go

出力は次のようになります。

Output
doAnother: 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つの数値を読み取った後、次のチャネル操作を待機します。 次に発生するのはdoSomethingcancelCtxを呼び出すことなので、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を使用するように更新します。

projects / contexts / main.go
...

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")
}

...

更新されたコードは、doSomethingcontext.WithDeadlineを使用して、time.Now関数を使用して関数が開始してから1500ミリ秒(1.5秒)後にコンテキストを自動的にキャンセルします。 コンテキストの終了方法を更新することに加えて、他のいくつかの変更が行われました。 cancelCtxを直接呼び出すか、期限を過ぎて自動キャンセルすることでコードを終了できる可能性があるため、doSomething関数が更新され、selectステートメントを使用して番号を送信するようになりました。チャネル。 このように、printCh(この場合はdoAnother)から読み取っているものがチャネルから読み取られておらず、ctx.Doneチャネルが閉じている場合、 [X149X ]関数もそれに気づき、番号の送信を停止します。

また、cancelCtxが2回呼び出されていることにも気付くでしょう。1回目は新しいdeferステートメントを介して、もう1回は以前の場所で呼び出されます。 defer cancelCtx()は、他の呼び出しが常に実行されるため、必ずしも必要ではありませんが、将来returnステートメントがあり、それが失われる場合に備えて、保持しておくと便利です。 。 コンテキストが期限からキャンセルされた場合でも、使用されたリソースをクリーンアップするためにキャンセル関数を呼び出す必要があるため、これはより安全な手段です。

次に、go runを使用してプログラムを再度実行します。

  1. go run main.go

出力は次のようになります。

Output
doAnother: 1 doAnother: 2 doAnother err: context deadline exceeded doAnother: finished doSomething: finished

今回の出力では、3つの数値すべてが出力される前に、context deadline exceededエラーのためにコンテキストがキャンセルされたことがわかります。 doSomething機能の実行開始から1.5秒後に期限が設定され、doSomethingは各番号の送信後1秒待機するように設定されているため、3番目の番号が印刷される前に期限を超えます。 期限を超えると、doAnotherdoSomethingの両方の機能が実行を終了します。これは、両方とも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.TimeAddメソッドを独自に使用して、特定の時間を終了する必要があります。

最後にもう一度、main.goファイルを開き、context.WithDeadlineの代わりにcontext.WithTimeoutを使用するように更新します。

projects / contexts / main.go
...

func doSomething(ctx context.Context) {
	ctx, cancelCtx := context.WithTimeout(ctx, 1500*time.Millisecond)
	defer cancelCtx()

	...
}

...

ファイルを更新して保存したら、go runを使用して実行します。

  1. go run main.go

出力は次のようになります。

Output
doAnother: 1 doAnother: 2 doAnother err: context deadline exceeded doAnother: finished doSomething: finished

今回プログラムを実行すると、context.WithDeadlineを使用したときと同じ出力が表示されます。 エラーメッセージも同じで、context.WithTimeoutは実際にはtime.Nowの計算を行うための単なるラッパーであることを示しています。

このセクションでは、context.Contextを終了する3つの異なる方法を使用するようにプログラムを更新しました。 最初のcontext.WithCancelでは、関数を呼び出してコンテキストをキャンセルできました。 次に、context.WithDeadlinetime.Timeの値を使用して、特定の時間にコンテキストを自動的に終了しました。 最後に、context.WithTimeouttime.Durationの値を使用して、一定の時間が経過した後にコンテキストを自動的に終了しました。 これらの機能を使用すると、プログラムがコンピューターで必要以上のリソースを消費しないようにすることができます。 それらがコンテキストを返す原因となるエラーを理解すると、独自のGoプログラムや他のGoプログラムでのエラーのトラブルシューティングも容易になります。

結論

このチュートリアルでは、Goのcontextパッケージをさまざまな方法で操作するプログラムを作成しました。 最初に、context.Context値をパラメーターとして受け入れる関数を作成し、context.TODOおよびcontext.Background関数を使用して空のコンテキストを作成しました。 その後、context.WithValueを使用して新しいコンテキストに値を追加し、Valueメソッドを使用して他の関数で値を取得しました。 最後に、コンテキストのDoneメソッドを使用して、コンテキストの実行がいつ完了したかを判別しました。 関数context.WithCancelcontext.WithDeadline、およびcontext.WithTimeoutと組み合わせると、独自のコンテキスト管理を実装して、これらのコンテキストを使用するコードの実行時間に制限を設定します。

より多くの例でコンテキストがどのように機能するかについて詳しく知りたい場合は、Go contextパッケージのドキュメントに追加情報が含まれています。

このチュートリアルは、 DigitalOcean How to Code inGoシリーズの一部でもあります。 このシリーズでは、Goの初めてのインストールから、言語自体の使用方法まで、Goに関する多くのトピックを取り上げています。