🏎️

【Go腕試し】シンプルなcontext処理

に公開

What's this?

Go の入門者から上級者まで幅広く多くの Gopher が Go を楽しみつくすためのクイズを作りました。
ぜひチャレンジしてみてください!
今回の問題は簡単かもしれないです。

さっそく問題!

package main

import (
	"context"
	"fmt"
	"net/http"
	"time"
)

func handler(w http.ResponseWriter, r *http.Request) {
	ctx := r.Context()

	go func() {
		select {
		case <-time.After(5 * time.Second):
			fmt.Println("処理完了")
		case <-ctx.Done():
			fmt.Println("キャンセルされました")
		}
	}()

	fmt.Fprintf(w, "レスポンス返却")
}

func main() {
	http.HandleFunc("/", handler)
	http.ListenAndServe(":8080", nil)
}

このコードで curl http://localhost:8080 でリクエストを送った場合、何が出力されるでしょうか?

  1. 処理完了 が 5 秒後に出力される
  2. キャンセルされました がすぐに出力される
  3. 何も出力されない
  4. panic が発生する

※環境依存がない前提での出題ですが、何かあればごめんなさい。。。

回答・解説

答えはこちら

正解は 2 の キャンセルされました がすぐに出力される です!
正解できましたかね?
簡単だぜ!ってかたはごめんなさい。

解説はこんな感じです。

package main

import (
	"context"
	"fmt"
	"net/http"
	"time"
)

func handler(w http.ResponseWriter, r *http.Request) {
	ctx := r.Context()
	// r.Context() で取得できる context は、HTTP リクエストのライフサイクルに紐づいている
	// そのため、レスポンスが返却された直後、 context が自動的にキャンセルされる

	go func() {
		select {
		case <-time.After(5 * time.Second):
			fmt.Println("処理完了")
		case <-ctx.Done():
			// HTTP レスポンス返却後、すぐにこちらが実行される
			fmt.Println("キャンセルされました")
		}
	}()

	fmt.Fprintf(w, "レスポンス返却")
	// ここでレスポンスを返すと、handler 関数が終了する
	// handler 関数が終了すると、r.Context() もキャンセルされる
	// そのため、goroutine 内の ctx.Done() が即座に閉じられる
}

func main() {
	http.HandleFunc("/", handler)
	http.ListenAndServe(":8080", nil)
}

重要なポイント

  1. HTTP リクエストの context はリクエストのライフサイクルに紐づく

    • r.Context() は HTTP レスポンス返却時に自動的にキャンセルされる
    • これはクライアントが切断した場合の処理中断を実現するための仕様
  2. バックグラウンド処理には独立した context を使うべき

    • 長時間実行される処理には context.Background() を使う
    • または context.TODO() で新しい context を作成する
  3. 正しい実装例

func handler(w http.ResponseWriter, r *http.Request) {
	// リクエストとは独立した context を作成
	ctx := context.Background()

	go func() {
		select {
		case <-time.After(5 * time.Second):
			fmt.Println("処理完了") // こちらが実行される
		case <-ctx.Done():
			fmt.Println("キャンセルされました")
		}
	}()

	fmt.Fprintf(w, "レスポンス返却")
}

このように実装すれば、HTTP レスポンス返却後もバックグラウンド処理が継続されます。

ただし、リクエストに関連する処理であれば、キャンセルを検知して中断すべきだと思います。
たとえば、DB クエリや外部 API 呼び出しなど、リクエストに紐づく処理では r.Context() を使うことで、クライアントが切断した際に無駄な処理を中止できるかなと思います。


References

さいごに

どうでしたか?
Gopher のみなさん、楽しんでもらえましたかね?
今回余裕の正解だった上級者の方はごめんなさい。次こそリベンジします!
今後も Go の勉強になるようなクイズを作りますので、次回のチャレンジもお待ちしております!

Discussion