元ネタは@lestrratさんの「Abusing type aliases to augment context.Context」。
golangを用いてHTTPサーバを作る場合、ルーティングを定義するのに以下の様な関数を用います。
http.HandleFunc(path string, handler func(w http.ResponseWriter, r *http.Request)
もちろん、http.Handle
を用いる場合もありますし、gorilla/muxなどのライブラリを用いることもあると思います。
ここで重要なのは、func(w http.ResponseWriter, r *http.Request)
という引数の方です。
多くの場合、アプリケーションのハンドラ内ではデータベースなどの外部アプリケーション・ミドルウェアを用いることになります。
しかし、golangのHTTPアプリケーションでは、ハンドラ関数の形式がfunc (w http.ResponseWriter, r *http.Request)
と決まっています。引数の追加はできないため、引数以外の方法でDB接続情報などを渡す必要があります。
これまで、golangでWebアプリケーション開発を行う場合によく用いられていたデータベースコネクションの保持方法は、db
パッケージを作成し、そこにパッケージ変数として持つ方法かと思います。が、グローバルな変数はできるだけ持ちたくないですよね。
そこで、Go 1.8から追加されたcontext
を使うことができます。http.Request
にはcontext.Context
が入っていて、Request.Context()
でget、Request.WithContext()
でsetできます。
context.Context
に値を持たせる方法で最初に思いつくのはContext.WithValue()
を用いる方法ですが、これは値を取得する度にtype assertionをする必要があり、あまりよくありません。
これを解消するため、自分で型を定義するのがよいでしょう。
package context // internal context subpackage
import (
"context"
"errors"
)
type withSomethingContext struct {
context.Context
something *Something
}
func WithSomething(ctx context.Context, something *Something) context.Context {
return &withSomethingContext{
Context: ctx,
something: something,
}
}
func Something(ctx context.Context) (*Something, error) {
if sctx, ok := ctx.(*withSomethingContext); ok {
if sctx.something != nil {
return sctx.something, nil
}
}
return nil, errors.New(`no asscosiated something`)
}
このように定義をすることで、毎回type assertionをする必要もなくなり、すっきりします。
扨、このパッケージとcontext
パッケージを両方読み込むためには、どちらかの読み込み名称を変更する必要があります。
たとえば、以下の様な具合です。
import (
"context"
mycontext "github.com/hoge/fuga/context"
また、ソースコード中でもcontext
とmycontext
を使い分ける必要があり、煩雑です。
この問題は、Go 1.9で導入されたType Aliasを使うときれいに書くことができます。
import "context"
type Context = context.Context
このように書くと、標準パッケージのcontext.Context
と、このアプリケーションにおけるcontext.Context
が同一のものとして扱われます。
そのため、一つのパッケージのインポートだけで良くなります。
最終的なcontext
サブパッケージのコードは以下の様になるでしょう。
package context // internal context subpackage
import (
"context"
"errors"
)
type Context = context.Context
/*
** some more definition
*/
type withSomethingContext struct {
Context
something *Something
}
func WithSomething(ctx Context, something *Something) Context {
return &withSomethingContext{
Context: ctx,
something: something,
}
}
func Something(ctx Context) (*Something, error) {
if sctx, ok := ctx.(*withSomethingContext); ok {
if sctx.something != nil {
return sctx.something, nil
}
}
return nil, errors.New(`no asscosiated something`)
}
実際には、標準パッケージのcontext
と同等に使用するにはその他の定義の再定義や、複数のwithXXXContext
を定義した場合には再帰的に値を読み出す処理が必要になりますが、基本的にはこの形を使用すると便利です。
このようにcontext
を定義しておき、以下の様に使用します。
func withSomethingMiddleware(h http.Handler) http.Handler {
return http.Handler(func(w http.ResponseWriter, r *http.Request) {
something := &Something{}
r = r.WithContext(context.WithSomething(r.Context(), something))
h.ServeHTTP(w, r)
})
}
http.Handler
のmiddlewareについてはまたの機会に。
Top comments (0)