DEV Community

nasa9084
nasa9084

Posted on • Originally published at blog.web-apps.tech on

Application Specific Context

#go

Application Specific Context

元ネタは@lestrratさんの「Abusing type aliases to augment context.Context」。

golangを用いてHTTPサーバを作る場合、ルーティングを定義するのに以下の様な関数を用います。

http.HandleFunc(path string, handler func(w http.ResponseWriter, r *http.Request)
Enter fullscreen mode Exit fullscreen mode

もちろん、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`)
}
Enter fullscreen mode Exit fullscreen mode

このように定義をすることで、毎回type assertionをする必要もなくなり、すっきりします。

扨、このパッケージとcontextパッケージを両方読み込むためには、どちらかの読み込み名称を変更する必要があります。

たとえば、以下の様な具合です。

import (
    "context"
    mycontext "github.com/hoge/fuga/context"
Enter fullscreen mode Exit fullscreen mode

また、ソースコード中でもcontextmycontextを使い分ける必要があり、煩雑です。

この問題は、Go 1.9で導入されたType Aliasを使うときれいに書くことができます。

import "context"

type Context = context.Context
Enter fullscreen mode Exit fullscreen mode

このように書くと、標準パッケージの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`)
}
Enter fullscreen mode Exit fullscreen mode

実際には、標準パッケージの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)
    })
}
Enter fullscreen mode Exit fullscreen mode

http.Handlerのmiddlewareについてはまたの機会に。

Top comments (0)