はじめに
Goで middleware からハンドラ関数に値を渡したい場合,contextを使って以下のように書くことが多いと思います.
import (
"context"
"net/http"
)
func exampleMiddleware(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ctx := context.WithValue(r.Context(), "hoge", hoge)
next.ServeHTTP(w, r.WithContext(ctx))
}
}
func exampleHandlerFunc(w http.ResponseWriter, r *http.Request) {
v := (r.Context()).Value("hoge")
hoge, ok := v.(string)
}
今日は,このコードの理解を深めようと思います.
context とは
contextのパッケージ によると,context の概要は以下の通りです.
Package context defines the Context type, which carries deadlines, cancellation signals, and other request-scoped values across API boundaries and between processes.
(和訳)Package contextはContext型を定義し、デッドライン、キャンセルシグナル、その他のリクエストスコープに含まれる値をAPI境界やプロセス間で伝達します。
今回の例だと,context は「リクエストスコープに含まれる値をAPI境界やプロセス間で伝達する」ために使われていそうです.
context を用いて middlewareからハンドラ関数に値を渡すときのフロー
context を用いて middlewareからハンドラ関数に値を渡すとき,以下のようなフローになっていることが分かりました.
- (middleware) 渡したい値が含まれる context の生成
- (middleware) 1 で生成した context をrequestにセット
- (ハンドラ関数) request にセットされている context から値を取り出す.
import (
"context"
"net/http"
)
func exampleMiddleware(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ctx := context.WithValue(r.Context(), "hoge", hoge) // 1
next.ServeHTTP(w, r.WithContext(ctx)) // 2
}
}
func exampleHandlerFunc(w http.ResponseWriter, r *http.Request) {
v := (r.Context()).Value("hoge") // 3
hoge, ok := v.(string)
}
それぞれ詳しく説明していきます.
1. 渡したい値が含まれるcontext の生成
まず,middleware 内で以下を実行することにより,渡したい値(ここではhoge
)が含まれるcontext ctx
を生成します.
// r *http.Request
ctx := context.WithValue(r.Context(), "hoge", hoge)
context.withValue
関数の概要は,contextのパッケージ によると以下の通りです.
WithValue returns a copy of parent in which the value associated with key is val.
(和訳)WithValue は、key に関連付けられた値を val とする parent のコピーを返す。
ここでのコピー元 parent は r.Context()
ということになるのですが, r.Context()
について調べてみます.
net/httpのrequest.goのソースコードを見てみると,Request 構造体にcontextが定義されていました.
type Request struct {
...
ctx context.Context
}
r.Context()
は,このRequest構造体に定義されてるcontextを取ってくるようです.
func (r *Request) Context() context.Context {
if r.ctx != nil {
return r.ctx
}
return context.Background()
}
2. 1 で生成したcontextをrequestにセット
次に,以下のコードの r.WithContext(ctx)
において,1で生成したcontext ctx
をrequestにセットしています.
next.ServeHTTP(w, r.WithContext(ctx))
ちなみに,r.WithContext(ctx)
のソースコードは以下のようになっています.
func (r *Request) WithContext(ctx context.Context) *Request {
if ctx == nil {
panic("nil context")
}
r2 := new(Request)
*r2 = *r
r2.ctx = ctx // ここで新たなctxをrequestにset
return r2
}
3. requestにセットされているcontextから値を取り出す.
最後に,ハンドラ関数内の以下の部分で,requestにセットされているcontextから値を取り出します.
v := (r.Context()).Value("hoge")
r.Context()
で,requestにセットされているcontextを取得して,Value("hoge") で値を取り出しています.
以上のフローで, request にセットされた context を介して,middlewareからハンドラ関数に値を渡すことができるみたいです.
最後に
今回は,context を使って middleware からハンドラ関数に値を渡したい場合について,ソースコードを見ながらフローを追ってみました!これを見て,私と同じように少しでも理解が深まった方がいたらうれしいです!
参考文献
https://pkg.go.dev/context
https://github.com/golang/go/blob/master/src/net/http/request.go
https://moneyforward.com/engineers_blog/2020/07/28/go-context/