Chapter 19無料公開

コンテキスト

context

Webサーバーを構築する直接の技術ではないですが、必要になると思うので紹介します。contextというその英語の通り、テキストを一緒に(con)併せて渡していくことなのですが、実務上は並列処理をしていたときのタイムアウトに使われます。もちろん、データを後続に渡していくためだけに用いることができますが、使われ方としては情報としてユーザIDを組み合わせてログインしている人ごとに処理をする場合にユーザIDを併せて後続処理に渡していくような形になり、根本的な目的はタイムアウト処理となります。

contextの中身をみる

まずはそもそもcontextとは何なのか、中身を見てみましょう。

main.go
package main

import (
	"log"
	"net/http"
)

func main() {
	http.HandleFunc("/", foo)
	http.Handle("/favicon.ico", http.NotFoundHandler())
	http.ListenAndServe(":8080", nil)
}

func foo(w http.ResponseWriter, req *http.Request) {
	ctx := req.Context()
	log.Println(ctx)
}
2020/11/03 15:28:36 context.Background.WithValue(type *http.contextKey, val <not Stringer>).WithValue(type *http.contextKey, val [::1]:8080).WithCancel.WithCancel

context.Background.WithValueとか.WithCancelの文字が見えます。実際にこれらを使っていきます。

データの渡し方

ではデータを渡してみましょう。

main.go
package main

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

func main() {
	http.HandleFunc("/", index)
	http.HandleFunc("/bar", bar)
	http.Handle("/favicon.ico", http.NotFoundHandler())
	http.ListenAndServe("localhost:8080", nil)
}

func index(w http.ResponseWriter, req *http.Request) {
	ctx := req.Context()

	ctx = context.WithValue(ctx, "userID", 123)
	ctx = context.WithValue(ctx, "fname", "akita")

	results := dbAccess(ctx)

	fmt.Fprintln(w, results)
}

func dbAccess(ctx context.Context) int {
	uid := ctx.Value("userID").(int)
	return uid
}

func bar(w http.ResponseWriter, req *http.Request) {
	ctx := req.Context()

	log.Println(ctx)
	fmt.Fprintln(w, ctx)
}


このように渡すことができました。
ですが、ここで改めて注意をしておきます。VSCodeを使っていると以下のwarningがでます。

go-lint先生が指摘しているように、値を渡すために用いられるのはよろしくありません。まぁ、普通に引数に渡して行けば良いのであえてこのような使い方をすることはないと思いますが。

必要になるケース

そしてようやく、タイムアウト処理の仕方です。まず次のコードをみてください。まずmain()でchanを作成し、go-routineを呼び出しています。go-routineではここでは単に5秒待たせています。イメージ的にはここでやや重い処理をするとか、ユーザーインプットが遅延しているとかそういう状況です。

main.go
package main

import (
	"fmt"
	"time"
)

func main() {
	ch := make(chan string)
	go foo(ch)

	for {
		select {
		case <-ch:
			fmt.Println("OK")
			return
		}
	}
}

func foo(ch chan string) {
	fmt.Println("start")
	time.Sleep(5 * time.Second)
	fmt.Println("end")
	ch <- "output"
}

実行すると、次のようになります。

start
(5秒待って)
end
OK

contextを使う

上の例では5秒待っていれば処理が進んでめでたく処理が完了しますが、もしフリーズするなど待てど暮らせど処理が返ってこない状況もあり得ます。その場合に、contextで待ち時間を設定して強制キャンセルをかけることができます。

main.go
package main

import (
	"context"
	"fmt"
	"time"
)

func main() {
	ch := make(chan string)
	// contextを設定する
	ctx := context.Background()
	ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
	defer cancel()

	go foo(ctx, ch) // ctxを渡す

	for {
		select {
		// タイムアウトになったときの処理
		case <-ctx.Done():
			fmt.Println(ctx.Err())
			return

		case <-ch:
			fmt.Println("OK")
			return
		}
	}
}

// 第1引数にコンテキストを渡す
func foo(ctx context.Context, ch chan string) {
	fmt.Println("start")
	time.Sleep(5 * time.Second)
	fmt.Println("end")
	ch <- "output"
}

タイムアウト時間を3秒にしたので、強制終了します。

start
context deadline exceeded