LoginSignup
1
1

More than 5 years have passed since last update.

Goのhttp.HandlerFuncをより扱いやすくするテクニック集

Posted at

GolagでHTTPリクエストを受け付けるとき、http.HandlerFuncという型の関数で処理を受け付けます。
シンプルなechoサーバーなどの実装であればこれで十分なのですが、実際アプリケーションを開発することを想定するとこの関数がもつ情報だけでは力不足なケースがほとんどです。

この投稿では、より開発しやすいようにリクエストを受け付ける関数の実装方法について記していきます。

普通にHTTPリクエストを受け取る処理

http.HandleFuncに受け付けるパスとそれに対応する処理関数(http.HandlerFunc)を与えます。

import (
    "net/http"
)

func main() {
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte("Hello world!!"))
    })

    http.ListenAndServe(":8080", nil)
}

アプリケーションのコンテキストを参照したい場合

通常、アプリケーションを作る時はアプリケーションのコンテキストのようなアプリケーションに関する基底情報を持ちます。
(例えば、アプリケーションの設定値だとか、アプリケーションの実行環境だとか)

これをhttp.HandlerFuncから愚直に参照しようとすると以下のようになるでしょうか?

import (
    "net/http"
)

var appCtx = map[string]interface{}{
    "env":    os.Getenv("Environment"),
    "config": &struct{}{},
}

func main() {
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        var res string
        if appCtx["env"] == "production" {
            res = "Hello production!!"
        } else {
            res = "Hello development!!"
        }

        w.Write([]byte(res))
    })

    http.ListenAndServe(":8080", nil)
}

グローバル変数としてappCtxを定義し、これを処理関数内から参照するようにします。
しかし、この方法にはいくつか問題があります。

1つはアプリケーションのコンテキストがリクエストユーザーによって情報が変わるケースに対応出来ないことです。
グローバル変数で定義しているため、アプリケーションの起動時に値が決定してしまいます。
ユーザーのリクエスト毎にアプリケーションのコンテキストを設定する余地がありません。

2つ目は、何らかの理由でグローバルに定義したappCtxを更新したいときにレースコンディションが発生してしまうことです。
1つ目の理由に関連するのですが、appCtxにユーザー情報を乗せて置きたいという場合に、
appCtxに設定したユーザー情報がアクセスしたユーザーのものである保証が持てなくなってしまいます。

リクエスト毎にアプリケーションのコンテキストを生成する

グローバルにアプリケーションのコンテキストを作ることには問題があることを説明しました。
この問題を解決するために高階関数という関数を利用します。

高階関数は関数を引数や返り値に取る関数のことです。
リクエスト処理関数を引数にとり、http.HandlerFunc型の関数を返すように出来れば問題を解決することが出来ます。

以下のようなリクエスト処理関数を定義します。

type AppContext = map[string]interface{}
type AppHandlerFunc = func(AppContext, http.ResponseWriter, *http.Request)

リクエスト処理関数を引数に取り、http.HandlerFuncを返す関数を定義します。

http.HandlerFuncの実装内部でアプリケーションのコンテキストを生成するため、他のリクエストからのレースコンディションを避けることが出来るようになります。

func CreateAppHandlerFunc(f AppHandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        ctx := AppContext{
            "env":    os.Getenv("Environment"),
            "config": &struct{}{},
        }

        f(ctx, w, r)
    }
}

このテクニックを覚えることで、他にも様々なことが出来るようになります。

http.ResponseWriterや*http.Requestの代わりに独自の型を利用する

http.HandlerFuncが引数の取るhttp.ResponseWriter*http.Requestは一般的なサーバーアプリケーションの開発に利用するには力不足を感じます。
(例えば、JSONのレスポンスを返したいがそのための手順が煩雑になるなど)

このため、アプリケーションを構築するために即した独自のRequestやResponseのオブジェクトの必要性が高まってきます。
例えば以下のような…

type AppRequest struct {
    raw *http.Request
}

type AppResponse struct {
    raw http.ResponseWriter
}

func (res *AppResponse) sendJSON(data interface{}) {
    // JSONを送る処理
}

上記のオブジェクトはhttp.HandlerFuncの中で直接生成しても問題ありませんが、各リクエスト毎にこのコードを書くのか?と考えるとそれは煩雑です。

http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
    req := &AppRequest{raw: r}
    res := &AppResponse{raw: w}
    res.sendJSON(struct{}{})
})

こういった煩雑さを解消するためにも、先述したhttp.HandlerFuncを返す高階関数が利用できます。

type AppHandlerFunc = func(*Request, *Response)

func CreateHandlerFunc(f AppHandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        req := &AppRequest{raw: r}
        res := &AppResponse{raw: w}
        f(req, res)
    }
}

http.HandleFunc("/", CreateHandlerFunc(func(_ *Request, res *Response) {
    res.sendJSON(struct{}{})
})

前後に処理を挟めるようにする

WAFを利用すると、よくリクエストを処理する前と後ろに何らかの処理を挟むことが出来るかと思います。
ここでは、Actionというオブジェクトを定義してこれらの機能を実現します。

以下のようなインターフェースを定義します。
Beforeは前処理、Afterは後処理、Dispatchが主処理といった処理を持つことを想定します。

type Action interface {
    Before()
    After()
    Dispatch()
}

先程定義したActionインターフェースに適合するように、適当なオブジェクトを用意します。

/*
    FooAction
*/

type FooAction struct {
    w http.ResponseWriter
    r *http.Request
}

func NewFooAction(w http.ResponseWriter, r *http.Request) Action {
    return &FooAction{
        w: w,
        r: r,
    }
}

func (a *FooAction) Before() {}
func (a *FooAction) After() {}
func (a *FooAction) Dispatch() {
    a.w.Write([]byte("Foo"))
}

/*
    BarAction
*/

type BarAction struct {
    w http.ResponseWriter
    r *http.Request
}

func NewBarAction(w http.ResponseWriter, r *http.Request) Action {
    return &BarAction{
        w: w,
        r: r,
    }
}

func (a *BarAction) Before() {}
func (a *BarAction) After() {}
func (a *BarAction) Dispatch() {
    a.w.Write([]byte("Bar"))
}

各Action毎に使うか使わないか分からないBeforeAfterメソッドを実装するのは煩雑ですので基底オブジェクトを作って各Actionに埋め込みます。

type BaseAction {}
func (a *BaseAction) Before() {}
func (a *BaseAction) After() {}

/*
    FooAction
*/

type FooAction struct {
    *BaseAction
    w http.ResponseWriter
    r *http.Request
}

func NewFooAction(w http.ResponseWriter, r *http.Request) Action {
    return &FooAction{
        BaseAction: &BaseAction{},
        w:          w,
        r:          r,
    }
}

func (a *FooAction) Dispatch() {
    a.w.Write([]byte("Foo"))
}

/*
    BarAction
*/

type BarAction struct {
    *BaseAction
    w http.ResponseWriter
    r *http.Request
}

func NewBarAction(w http.ResponseWriter, r *http.Request) Action {
    return &BarAction{
        BaseAction: &BaseAction{},
        w:          w,
        r:          r,
    }
}

func (a *BarAction) Dispatch() {
    a.w.Write([]byte("Bar"))
}

ここまで出来たら最後にActionインターフェースを返す関数を引数にとって、http.HandlerFuncを返す高階関数を定義して完成です。

type ActionCreator = func(http.ResponseWriter, *http.Request) Action

func CreateHandlerFunc(f ActionCreator) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        a := f(w, r)
        a.Before()
        a.Dispatch()
        a.After()
    }
}

func main() {
    http.Handle("/foo", CreateHandlerFunc(NewFooAction))
    http.Handle("/bar", CreateHandlerFunc(NewBarAction))

    http.ListenAndServe(":8080", nil)
}

ここまでにいくつかのテクニックを紹介しましたが、どれも共通することはhttp.HandlerFuncを返す関数を定義することです。
このことさえ覚えておけばここで紹介したこと以外でも、様々なことが可能となります。

上手く活用して保守性の高いコードを書いていきたいですね!

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1