GoでのWebアプリケーションの組み立て方

  • 92
    いいね
  • 0
    コメント

はじめに

11月末に何故かアドベントカレンダーの一日目が空いていたので急遽この記事を作成することになりました。急いで書いたので不備などありましたらすみません。

新しい職場に転職後、業務としては初めてGoでWebアプリケーションを作成しました。
スクラッチで書かせてもらえたので、試行錯誤しながら一通り組むことができました。
また経験豊富な同僚からもアドバイスいただいたこともあり、
かなりGoのWebアプリの組み方について知見がたまりました。

そこで、MVCアプリケーションを組む上でどのような構成にしたらよいかについて、
特にコントローラ層を中心に自分の考えを紹介したいと思います。

MVCの設計

元々Rails等を書いていたので所謂WebのMVC(Model2)をベースに考えます。

View

書いたWebアプリケーションはAPIサーバだったので、実はあまりここについて出せる知見はありません。
たとえViewのあるアプリを作ったとしても、自分なら、ViewはReact等をつかってフロントエンド・アプリケーションとして実装し、Goの部分はAPIサーバとしての役割のみ担わせると思います。

しかし、もしサーバサイドでHTMLを生成する必要がある場合は、標準のtemplateパッケージを活用するとよいと思います。多機能ではないですがシンプルで使い勝手のよいテンプレートエンジンです。

Viewではありませんが、自分は設定ファイルに環境変数を埋め込む用途にこのtemplateパッケージを利用しています。

Controller

コントローラ層については、まずフレームワークを使うかどうかを考えるとよいでしょう。
多くのGoのマイクロフレームワークというのは、実際にはコントローラの部分をメインに実装しているようです。

それで使うかどうかですが、フレームワークについては僕は使わない方針です。
何故かと言うと所謂Webアプリケーションのコントローラに必要な機能というのは大方標準のパッケージで提供されていて、それで十分だし、なるべく標準パッケージを活用してシンプルに済ませるほうが僕はGo Wayらしいかなと思います。

コントローラ層に属すると僕が考えるものには以下があります。

  • リクエストハンドラ
  • ルーティング
  • フィルター
  • リクエストコンテキスト

リクエストハンドラ

リクエストハンドラの役割はリクエストを処理してレスポンスを返すことです。
モデル層の処理を呼び出してビューに伝えるのもリクエストハンドラの中で行われることになります。
リクエストハンドラについては標準のhttpパッケージで定義されているhttp.Handlerインターフェースに従えばよいかと思います。

僕がフレームワークを使わない理由の一つとして、よく使われているフレームワークの幾つかはhttp.Handlerインターフェースを備えていないハンドラを使っているので、標準のパッケージと互換性がないことをデメリットに感じたということも挙げられます。

ちなみに余談ですが、Goでは関数型にもメソッドを生やすことが出来るので、http.HandlerFunc型にキャストできる関数であれば、http.Handlerインターフェースをもたせることが出来ます。
いちいち構造体を定義したくないときはhttp.ServeHTTPメソッドの代わりに関数を一個作って、
その関数をhttp.HandlerFunc型にキャストするとその関数オブジェクトはhttp.Handlerを備えており、そのままハンドラーとして使うことが出来ます。

func function(w http.ResponseWriter, r *http.Request) {
  //この型の関数は`http.HandlerFunc`と引数、戻り値が同じなのでキャストできる。
  //`http.HandlerFunc`は`http.Handler`インターフェースを持たせているため、
  //関数をそのままハンドラーとして使うことが出来る。
}

ルーティング

Webアプリケーションにおけるコントローラの役割というのは、
ユーザ入力であるHTTPリクエストとモデルを媒介し紐付ける存在です。
そう考えるとルーティング処理というのがコントローラの重要な役割だと思います。

GoのWebアプリケーションでルーティング処理を受け持つ機能をMuxと呼んだりルータと呼んだりします。
この部分だけは標準のもの以外を利用しています。
というのも標準のnet/http付属のルーターは使い勝手があまりよくないからです。
とはいえシンプルなアプリであれば十分だったりするのですが。

ルーター・ライブラリはhttp.Handlerインターフェースに対応したものがよいです。
僕が使っているのはchiです。
標準インターフェースを採用しているルータのうち他にメジャーなものとしてはGorillaライブラリのMuxがよく使われていると思います。

フィルター

ハンドラをラップしてリクエストとレスポンスを修飾する機能をまとめてフィルタと呼びます。
フィルターの例としてはリクエストログや認証処理が挙げられます。
フィルターについてはMiddlewareと呼ばれるパターンで実装するのがよいでしょう。
Middlewarehttp.Handlerを受取、http.Handlerを返すような関数です。

func filter(next http.Handler) http.Handler {
  // http.HandlerFuncは関数にhttp.Handlerインターフェースをもたせた型で、
  // それにキャストすることでhttp.Handlerとして返している
  return http.HandlerFunc(function(w http.ResponseWriter, r *http.Request) {
    //前処理
    next(w, r) // ラップする対象のhttp.Handler
    //後処理
  })
}

基本的にMiddleware関数でハンドラをラップしていくのがフィルターの主要な書き方です。

ライブラリやフレームワークによってはフィルターをルーターに追加したり、
フィルター・チェーン(ラッピングの連鎖)を簡易に書けたりするような機能を提供しているものもあります。

リクエストコンテキスト

リクエストハンドラやフィルターを階層的に使っているとリクエスト全体で共有したい変数が出てきます。例えばデータベースのハンドラやセッション、リクエストを特定するIDなどです。
それらの変数を保持する構造体をリクエストコンテキストとよんでいます。

僕はプロセスでグローバルに共有される変数もグローバル変数ではなくリクエストコンテキストにセットするようにしています。グローバル変数が無いとテストのしやすさが全然違ってくるからです。

リクエスト内でのリクエストコンテキストの保持については標準のcontextパッケージを使えばいいと思います。
Go 1.7以降であればリクエスト変数自体からリクエスト毎のcontext.Contextを取り出せるので、それにリクエストコンテキストを格納します。

ただしcontext.Context構造体自体にリクエストコンテキストの役割を持たせて、個々の変数を格納してしまうのはおすすめしません。
context.Contextinterface{}型を保持するコレクションのように使えます。
逆に言うと保持されている変数はinterface{}型なので取り出すときにnilチェックとキャストが必要になります。
これが煩雑で管理しづらくなるので、リクエスト・コンテキストについてはひとつの構造体にまとめてしまえばキャストは一回で済みます。
取り出した構造体のメンバであれば型がついているのでいちいちキャストする必要がなくなるからです。
静的型付けはGoの長所なのでなるべく型情報が失われるのは避けたいところです。

ちなみにinterface{}型のキャストとnilチェックは若干複雑で、

v, ok := context.Value(key).(ValueType)
if !ok || v == nil {
  //空のときの処理
}

のようにする必要があります。
これは「interface{}のnil値」≠「何かの型のnil値をinterface{}型にキャストした値」であるため、interface{}型のnilチェックでは後者のnil値を検出できないためです。

コントローラ層が長くなってしまいましたがこの部分が概ね設計で迷いやすい部分ではあると思います。
モデルなどは特に特別な構成など考えず、Web以外の普通のGoプログラムと同じように設計していけばよいのではないかと思っています。

Model

モデルについては人によって組み立て方が別れる部分です。
GoではRailsのActiveRecordのようなモデル作成の中心となるライブラリがないので
わりと自由に組んでしまっていいのではないかと思います。

人によっては何かしらORMapperを採用したりするかもしれませんが、
僕は標準のdatabase/sqlでデータアクセス層を組んでしまいました。
なのであんまりここでお伝えできることはないです。

いくつか僕が気をつけている点だけ書き出すと、

  • 入力値についてはインターフェース型を活用することでテスタビリティを上げたり疎結合にしたりしやすくなる
  • 無理にオブジェクト指向的に書かず、パッケージ・ローカルの関数を使って処理していく。パッケージの外からはある程度オブジェクトとして取り回しできればよい
  • ログについてはリクエストと紐付けたいので、モデル層ではエラーをログに吐かず、一旦リクエストハンドラに戻した上で書き出す

というようなことを気をつけています。

おわりに

MVCといいつつコントローラの話ばかりになってしまいましたが、
ある程度GoのWebアプリケーションの組み方について参考になったのではないでしょうか。
GoのWebアプリはこれといって定石がないので、最初のうちは迷うことが多いのではないかと思います。
これが唯一の正解というわけではないですが、皆さんの参考になれば幸いです。

この投稿は Go Advent Calendar 20161日目の記事です。