🕌

Goで現在時間を1リクエストごとに統一する

2020/09/27に公開1

以前、Qiita の方で Go でアプリケーションとクライアントのミドルウェアを作成する方法知ってますか?という記事の中で net/http を使ったアプリケーションハンドラのミドルウェアを作成する方法を紹介しました。

今回はミドルウェアを使って 1 リクエストで扱う時間を統一する方法を紹介します。とても簡単です。リクエスト単位で時間を取得できると例えば、何かしらのデータを複数のレコードを更新する際に updated_at なるカラムへ保存する時刻を揃えることが可能になります。

方法

方法はとてもシンプルで context.Context を使います。

  1. ミドルウェア内で http.Request が持つ context.Context に time.Now() を渡す。
  2. アプリケーションロジックで現在時刻を扱う時に http.Request の context.Context から保持してる time.Time を取り出す。

これだけを行う実際のコードを紹介します。

ミドルウェア

今回は ctxtime というようなパッケージを用意してみました。下記は仮想コードです。実際に動かすにはもろもろ import する必要があります。

package ctxtime

type contextKey struct{}

func Now(ctx context.Context) time.Time {
    tm, ok := ctx.Value(ctx, contextKey{})
    if !ok {
        return time.Time{} // この辺は好きに実装してください
    }
    return tm
}

func WithTime(ctx context.Context, t time.Time) context.Context {
    return context.WithValue(ctx, contextKey{}, t)
}

func CurrentTimeMiddleware() middleware.Middleware {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
	    now := time.Now()
            ctx := context.WithValue(r.Context(), contextKey{}, now)
            next.ServeHTTP(w, r.WithContext(ctx))
        })
    }
}

メインハンドラ

では実際に先程定義した middleware を組み合わせてハンドラを動かしてみます。

func main() {
	mux := http.NewServeMux()
	mux.Handle("/hello", middleware.Use(
		mainHandler(),
		ctxtime.CurrentTimeMiddleware(),
	))

	// httptest package を代わりに使ってます
	srv := httptest.NewServer(mux)
	defer srv.Close()
	_, err := http.Get(srv.URL)
	if err != nil {
		log.Fatal(err)
	}
	// 実際に手元で動かしたいときはコメントアウトして
	// 動かしてください
	//
	// log.Println(srv.URL)
	// time.Sleep(30 * time.Second)
}

func mainHandler() http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		ctx := r.Context()
		tm := ctxtime.Now(ctx)
		if !tm.IsZero() {
			log.Println("not zero value")
		}
		log.Println("First call", ctxtime.Now(ctx))
		log.Println("Second call", ctxtime.Now(ctx))
		log.Println("Final call", ctxtime.Now(ctx))
	})
}

動かしてみると時間がリクエストごとで統一されてることが確認できるはずです。 playground も用意したので、全体像を詳しく知りたい方はどうぞ。

https://play.golang.org/p/ifu5vcFCcm8

まとめ

  1. request がハンドラに到着する
  2. CurrentTimeMiddleware で現在時刻を取得、context.Context へ保存
  3. メインハンドラで context.Context から時刻を取得して使う