😎
GoのContextパターンガイド:よくある落とし穴とベストプラクティス
Contextの基本
そもそもContextって何?
リクエストに関するメタデータの受け渡しや、キャンセル制御などの課題を解決するための仕組みです。
Goにおける並列処理で有名な、ゴルーチンは一意なIDを持っていません。そのため、リクエストをトラッキングできず、個別判定できません。
そこで、contextの出番です。
contextはAPIやプロセスの境界を超えて、次のデータを運搬することができます。
Contextが運搬できるデータと使うタイミング
- タイムアウト情報 (デッドライン)
- ある時刻まで、もしくはn秒かかったら処理を止めたいとき。
- キャンセレーションのシグナル
- 処理の中断をしたい時。HTTPリクエストがキャンセルされたとき、子処理も止めたい時。
- 処理に必要な値
- Cookieに格納されたセッションIDを後続の処理でも参照したいとき
実践 Contextの簡単な例
コミット)
キャンセレーションの使い方【例あり】(ctx, cancel := context.WithCancel(baseContext)
defer cancel()
コミット)
タイムアウトを設定する【例あり】 (ctx, cancel := context.WithTimeout(baseContext, 5*time.Second)
defer cancel()
親のコンテキストTimeout設定で2秒、子のコンテキストTimeout設定で5秒設定した場合、親で2秒Timeoutでcancelが発火し、子のコンテキストもcancelとなる。
select {
case <-time.After(5 * time.Second):
log.Println("end")
return c.String(http.StatusOK, "done")
case <-ctx.Done():
end := time.Now()
log.Println("canceled")
log.Println(end.Sub(start))
return ctx.Err()
}
ctxがキャンセルとなると、 ctx.Done()
の処理に入る
ファイル)
コンテキストで値を受け渡す【例あり】 (context.WithValueは、認証情報やトレーシングIDのような横断的関心事(cross-cutting concerns) を運ぶ手段として便利です。しかし、それがドメインロジックの中核に入り込むと問題になります。
// 値を受け取って使う側
func logWithRequestID(ctx context.Context, message string) {
if reqID, ok := ctx.Value(contextKey("request_id")).(string); ok {
log.Printf("[reqID=%s] %s", reqID, message)
} else {
log.Println(message)
}
}
// 値を渡す側
func handlerTrace(c echo.Context) error {
ctx := context.WithValue(c.Request().Context(), contextKey("request_id"), "abc_123")
logWithRequestID(ctx, "StartLogTraceHandler")
return c.String(http.StatusOK, "Logged with request ID")
}
Context管理のベストプラクティス
- ✅ context.WithValue キー設定
- コンテキストにセットするキーは独自の型を作成するのがBEST
- contextにセットする値は、同様のキーの型・名称の場合上書きされるためです
- コンテキストにセットするキーは独自の型を作成するのがBEST
- ✅ context.WithValueのユースケース
- CookieやセッションIDやリクエストメタデータ、リクエスト追跡用のID共有に使うのがベスト
package main
import (
"context"
"github.com/labstack/echo/v4"
"log"
"net/http"
)
type contextKey string
func logWithRequestID(ctx context.Context, message string) {
if reqID, ok := ctx.Value(contextKey("request_id")).(string); ok {
log.Printf("[reqID=%s] %s", reqID, message)
} else {
log.Println(message)
}
}
func handlerTrace(c echo.Context) error {
ctx := context.WithValue(c.Request().Context(), contextKey("request_id"), "abc_123")
logWithRequestID(ctx, "StartLogTraceHandler")
return c.String(http.StatusOK, "Logged with request ID")
}
func main() {
e := echo.New()
e.GET("/", handlerTrace)
if err := e.Start(":9010"); err != nil {
log.Fatalf(err.Error())
}
}
- ✅ コンテキストの共有
- コンテキストは1リクエスト1つを共有する
- キャンセルシグナルが発生した際に共通化でき、不要な処理を中断することで、リソースの効率化につながります
- コンテキストは1リクエスト1つを共有する
package main
import (
"context"
"fmt"
"github.com/labstack/echo/v4"
"log"
"net/http"
"time"
)
func slowOperation(ctx context.Context) (string, error) {
select {
case <-time.After(5 * time.Second):
return "success", nil
case <-ctx.Done():
return "failed", ctx.Err()
}
}
func handler(c echo.Context) error {
ctx := c.Request().Context()
result, err := slowOperation(ctx)
if err != nil {
fmt.Println("canceled")
return c.String(http.StatusRequestTimeout, "request canceled : "+err.Error())
}
return c.String(http.StatusOK, result)
}
func main() {
e := echo.New()
e.GET("/", handler)
if err := e.Start(":9010"); err != nil {
log.Fatalf(err.Error())
}
}
Context管理のバッドプラクティス
- ❌ context.WithValue キー設定
- キー名をstring型で指定している
- 他の処理でctxに値を格納した際に、キー名が重複した場合、値が上書きされる可能性が高く、バグを生む危険性が高まります
- キー名をstring型で指定している
- ❌ context.WithValueのユースケース
- ビジネスロジックにctxをそのまま渡し、値を参照している
- ビジネスロジックがctxに処理を依存してしまうため、テストの実装が難しくなり保守性が下がります
- ビジネスロジックにctxをそのまま渡し、値を参照している
package main
import (
"context"
"errors"
"fmt"
"github.com/labstack/echo/v4"
"net/http"
)
func businessLogic(c context.Context) error {
if userID, ok := c.Value("user_id").(string); ok {
fmt.Printf("userID={%s}", userID)
return nil
} else {
fmt.Printf("user id none")
return errors.New("user id none")
}
}
func handler(c echo.Context) error {
// key is not correct.
ctx := context.WithValue(c.Request().Context(), "user_id", "bad_practice_name")
// ctx send to business logic
if err := businessLogic(ctx); err != nil {
return c.String(http.StatusInternalServerError, err.Error())
}
return c.String(http.StatusOK, "success")
}
func main() {
e := echo.New() // with value 値の重複
e.GET("/", handler)
e.Start(":9011")
}
Discussion