goでWebサーバを書くためのシンプルなライブラリchiの紹介

この記事は Go3 Advent Calendar 2017 の13日目の記事です。

はじめに

goでwebサーバを書く際にはいろいろやり方がありますが、ざっくり分けて以下のような感じだと思います。

  1. net/http で十分。必要に応じてルーティングに gorilla/mux 使ったりする
  2. 軽めのwebフレームワークを利用する。 gin, echo, gojiなどを使う
  3. 全部入りの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 でも使えました。