contextとは
そもそもcontextってどういう意味なのでしょうか?
contextは日本語で文脈、脈略という意味だそうです。このようにgoで使われる意味としても文脈、脈略を意味しています。goの公式ではこのような概要になっています。
パッケージ コンテキストは、期限、キャンセル信号、およびその他の要求スコープの値を API 境界を越えて、またプロセス間で伝達する Context タイプを定義します。
出典:https://pkg.go.dev/context#pkg-overview
ここで書かれていることをまとめると
- 期限の伝達
- キャンセル信号の伝達
- 要求スコープの値の伝達
ができるということです。
使い方
contextを使わない例
func main(){
userID := "user123"
timeout := 5 * time.Second
processRequest(userID, timeout)
}
func processRequest(userID string, timeout time.Duration) {
validateUser(userID, timeout)
fetchData(userID, timeout)
sendResponse(userID, timeout)
}
func validateUser(userID string, timeout time.Duration) {
//さらに深い関数にも渡す必要...
checkDatabase(userID, timeout)
}
最初にprocessRequestにuserIDを渡して、walidateUserにもuserIDを渡して、、、という風にバケツリレーが行われるようになります。
contextを使う例
func main() {
ctx := context.Background()
ctx = context.WithValue(ctx, "userID", "user123")
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
processRequest(ctx)
}
func processRequest(ctx context.Context) {
validateUser(ctx)
fetchData(ctx)
sendResponse(ctx)
}
func validateUser(ctx context.Context) {
//必要な情報をコンテキストから取得
userID := ctx.Value("userID").(string)
//タイムアウトやキャンセルも自動で管理
select {
case <-ctx.Done():
return //キャンセルされた
default:
checkDatabase(ctx)
}
}
ここで起きているcontextの処理を説明していきます。
context.Background()
contextでは親と子の関係を持ちます。そこで最初に親の空コンテキストを作成します。
特徴
- 何も持たない
- キャンセルされない(ctx.Done()は常にnil)
- すべてのコンテキストの出発点
があります。
context.WithValue
親コンテキストに値を追加した子コンテキストを作成します。この際、親子の関係を持っているため、最初に作成された子コンテキストである、userIDの値はrequestIDの処理にエラーが出た場合でも処理が続けられます。
//値を作成
ctx := context.Background()
ctx = context.WithValue(ctx, "userID", "user123")
ctx = context.WithValue(ctx, "requestID", "req-456")
//値を取得
userID := ctx.Value("userID").(string) //"user123"
requestID := ctx.Value("requestID").(string) //"req-456"
unknown := ctx.Value("unknown") //nil
ctx.Done
contextがキャンセルされた時に閉じられるチャネルを返します。処理を見た方が早いので以下の例を見ていきましょう。
ステップ1:普通のチャネル
done := make(chan struct{})
//別のgoroutineでチャネルを閉じる
go func() {
time.Sleep(2*time.Second)
close(done) //これで<-doneが反応する
}()
<-done //<-doneが反応したら"完了!"の文字
fmt.Println("完了!")
ステップ2:selectと組み合わせ
done := make(chan struct{})
go func() {
time.Sleep(2*time.Second)
close(done)
}()
select {
case <-done:
fmt.Println("doneが閉じられた!")
case <-time.After(5*time.Second):
fmt.Println("5秒経過")
}
ステップ3:contextのDone()
ctx, cancel := context.WithCancel(context.Background())
go func() {
time.Sleep(2*time.Second)
cancel() //これでctx.Done()が閉じられる
}()
select {
case <-ctx.Done():
fmt.Println("コンテキストがキャンセルされた!")
case <-time.After(5*time.Second):
fmt.Println("5秒経過")
}
context.WithTimeout
親コンテキストにタイムアウト機能を追加
func main() {
ctx := context.Background()
ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
defer cancel()
fmt.Println("開始:", time.Now().Format("15:04:05"))
select {
case <-ctx.Done():
fmt.Println("タイムアウト:", time.Now().Format("15:04:05"))
fmt.Println("エラー:", ctx.Err())
case <-time.After(5*time.Second):
fmt.Println("5秒経過")
}
}
最初に"開始:(現在の時刻)"が映し出されます。3秒後に"タイムアウト:(さっきの時刻から3秒後)"が映し出されます。そして"エラー:context deadline exceeded"が出力されて処理が終了します。
では、先ほどの例を見ていきましょう。ここで示される処理は先ほどと全く同じ処理となります。
func main() {
ctx := context.Background() //空のコンテキストを作成
ctx = context.WithValue(ctx, "userID", "user123") //値を挿入
ctx, cancel := context.WithTimeout(ctx, 5*time.Second) //タイムアウト処理を追加
defer cancel() //5秒後に<-doneになる
processRequest(ctx)
}
func processRequest(ctx context.Context) {
validateUser(ctx)
fetchData(ctx)
sendResponse(ctx)
}
func validateUser(ctx context.Context) {
//必要な情報をコンテキストから取得
userID := ctx.Value("userID").(string)
//タイムアウトやキャンセルも自動で管理
select {
case <-ctx.Done():
return //5秒後に処理が終了
default:
fmt.Printf("userID %v",userID)
}
}
5秒後にctx.Done()になるので、5秒を待たずにuserIDが返されて処理が終了します。こうすることによって、バケツリレーのようにする必要がありません。
contextはバケツリレーを防ぐほか、ゴルーチンで役割が果たされることが多いです。
func worker(ctx context.Context) {
for {
select {
case <-ctx.Done():
fmt.Println("ワーカー停止")
return //ここで終了
default:
doSomething()
time.Sleep(1 * time.Second)
}
}
}
func main() {
ctx, cancel := context.WithCancel(context.Background()) //cansel()で<-ctx.Done()が実行できるようにしている
go worker(ctx)
time.Sleep(5 * time.Second)
cancel() //ワーカーを停止
time.Sleep(1 * time.Second) //停止を待つ
}
ここで私が疑問に思ったのはチャネルでも同じ処理ができるのでは、、、?ということでした。そこでチャネルとの比較をしてより具体的にcontextの役割を理解していきましょう。
contextとチャネルの比較
1,構想管理
contextは自動的に親子関係を管理する一方でチャネルは手動で管理する必要があります。
//contextで管理
parent := context.Background()
child1, cancel1 := context.WithTimeout(parent, 5*time.Second)
child2, cancel2 := context.WithTimeout(child1, 3*time.Second)
//child2がキャンセルされても、child1は影響を受けない
//チャネルで管理
done1 := make(chan struct{})
done2 := make(chan struct{})
//関係性はない
2,値の伝達
contextは値の伝達が容易にできるようになります。
//context: 値を簡単に伝播
ctx := context.WithValue(context.Background(), "userID", "user123")
ctx = context.WithValue(ctx, "requestID", "req456")
func someFunc(ctx context.Context) {
userID := ctx.Value("userID").(string) //値を取り出す
}
//チャネル: 構造体で管理が必要
type RequestData struct {
UserID string
RequestID string
Done chan struct{}
}
3,エラー情報
エラーの内容を詳しく発見することができます。
//context: エラー理由が明確
select {
case <-ctx.Done():
switch ctx.Err() {
case context.DeadlineExceeded:
fmt.Println("タイムアウト")
case context.Canceled:
fmt.Println("キャンセル")
}
}
//チャネル: エラー理由が不明
select {
case <-done:
}
4,標準ライブラリとの連携
contextでは標準のライブラリが対応されており、連携が可能になっています。例えば、http.NewRequestWithContextを使うことでhttpリクエストにcontextを関連付けることができます。
チャネルの利点
- 単純な通信
- カスタムな制御が可能
contextの利点
- 階層的な管理
- 標準ライブラリとの連携
- 実用的な点
といったところでしょうか?
まとめ
contextは値を伝達するのに役立つパッケージです。個人的にreactでいうpropsっぽいなーと思いました。