この記事は Go3 Advent Calendar 2017 の13日目の記事です。
はじめに
goでwebサーバを書く際にはいろいろやり方がありますが、ざっくり分けて以下のような感じだと思います。
-
net/http
で十分。必要に応じてルーティングに gorilla/mux 使ったりする - 軽めのwebフレームワークを利用する。 gin, echo, gojiなどを使う
- 全部入りのrailsみたいなやつが欲しい。Revel などを使う
パフォーマンスとか書きやすさとかそれぞれ違うので、各自好きなの使えばいいと思います。ちなみに自分は、一つ前のプロジェクトでは gojiを使っていて、今はechoを使っています。
個人的にはechoよかったんですが、 GoogleAppEngineで go1.8と echoのver.3以降で使おうと思うとcontextの扱いがいまいちきれいに書けない感じになりそうなので、別の選択肢を探してました。
chiの特徴
go-chi/chi: lightweight, idiomatic and composable router for building Go HTTP services
chiは、上記の分類でいうと1に近い選択肢になるかと思います。echoに比べると機能は少ないですが、余計なことをしないのでいいという感じがします。routerとmiddlewareの機能を提供する薄いライブラリです。以下で簡単に説明します。
シンプルで薄い、速い
READMEによると
- 軽量
- 速い(benchmark)
- net/http互換
- 外部ライブラリに依存しない
ということです。
外部ライブラリに依存していないのはいろいろなライブラリのバージョンなど気にしなくてよくなるのでいいなと思いました。
また、net/http
と互換性があるので、chiを使うのをやめたくなったとしても、ルーティングの部分だけ取り除けばhandler以下はそのまま動かせるのもいいですね。
routing
基本的なルーティングは以下のように書けます。
{
r := chi.NewRouter()
// "articles"以下のURLをルーティング
r.Route("/articles", func(r chi.Router) {
r.Post("/", createArticle) // POST /articles
r.Get("/search", searchArticles) // GET /articles/search
// 正規表現を使ったURLパラメータも可能:
r.Get("/{articleSlug:[a-z-]+}", getArticleBySlug) // GET /articles/home-is-toronto
// サブルータ:
r.Route("/{articleID}", func(r chi.Router) {
r.Get("/", getArticle) // GET /articles/123
r.Put("/", updateArticle) // PUT /articles/123
r.Delete("/", deleteArticle) // DELETE /articles/123
})
})
http.ListenAndServe(":3000", r)
}
URLパラメータはもちろん受けられますし、サブルーターを使って、ルーティングをグループ化することもできます。
ルーティング先となるhandlerは、net/http
互換ということで、次のように書けます。
func getArticle(w http.ResponseWriter, r *http.Request) {
articleID := chi.URLParam(r, "articleID")
article, err := dbGetArticle(articleID)
if err != nil {
http.Error(w, http.StatusText(404), 404)
return
}
w.Write([]byte(fmt.Sprintf("title:%s", article.Title)))
}
また、次のようにMountを使ってルータを分けることもできます。
{
r := chi.NewRouter()
...
// サブルータでマウント
r.Mount("/admin", adminRouter())
}
// メインのルータとは独立したルータ
func adminRouter() http.Handler {
r := chi.NewRouter()
r.Use(AdminOnly) // ミドルウェアで認証(後述)
r.Get("/", adminIndex)
r.Get("/accounts", adminListAccounts)
return r
}
基本的なルーティングは以上です。
Middleware
chiのミドルウェアはnet/http
のミドルウェアなので、シンプルに http.Handler
を受けて何か処理をして、次の http.Handler
を返すだけです。
次の例のような感じです。
// contextに値をセットするmiddlewareの例
func MyMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := context.WithValue(r.Context(), "user", "123")
next.ServeHTTP(w, r.WithContext(ctx))
})
}
middlewareを使うことで、認証をしたり前処理をして値を context
に入れて後段の handler
に渡したり・・・など、さまざまなことが可能です。公式で提供されているmiddlewareや他の人がコミュニティに共有しているmiddlewareもいくつかあります。
middlewareを使う場合には、ルーティングの部分で次のように指定してやります。
r := chi.NewRouter()
// 公式提供のmiddleware
r.Use(middleware.RequestID)
r.Use(middleware.RealIP)
r.Use(middleware.Logger)
r.Use(middleware.Recoverer)
// 独自のmiddleware
r.Use(MyMiddleWare)
r.Route("/admin", func(r chi.Router) {
// 管理画面だけ認証する
r.Use(AdminOnly)
r.Get("/", adminIndex)
r.Get("/accounts", adminListAccounts)
})
}
//管理画面認証するためのMiddleware
func AdminOnly(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
perm, ok := ctx.Value("acl.permission").(YourPermissionType)
if !ok || !perm.IsAdmin() {
http.Error(w, http.StatusText(403), 403)
return
}
next.ServeHTTP(w, r)
})
}
middlewareを使うことで、共通の前処理などがきれいに書くことができます。
まとめ
以上が簡単ですが chi
の紹介です。
goでサーバを書く際に、なるべくフレームワークは使いたくないけど、きれいに書けるところは書きたい、という人にはおすすめできると思いますので、試してみてください。GAE/GO1.8 でも使えました。