tjun月1日記

なんでもいいので毎月書きたい

今からgoでwebサーバ書くならchiがいいかも

goでwebサーバを書く時、フレームワーク的なもののデファクトがいまいちない感じですが、chiを触ってみたらよさそうだったので紹介します。

これまでのgoでのWeb開発

去年くらいに調べたときの感じでは、

  • 標準のnet/httpでいいでしょ + routerに gorilla/muxみたいな薄いライブラリを入れる
  • 比較的軽めのframeworkで、 echo, Gin, goji など
  • Railsみたいなのが欲しい人はrevelとか beegoとか?

という感じでした。

個人的には、goで書くならあまり重いフレームワークは使いたくないけど net/httpはしんどそう、ということで今までは echo使ってました。結構よかったです。contextを引き回しておけば、そこから必要なものが取得できていい感じに書けました。

echoよかったけど・・・

echoよかったんですが、今から使おうと思うと自分の場合以下の点が気になりました。

  • contextの取り扱いが echo ver.2 から ver.3で変わっていて、AppEngine+go1.8で使おうと思うといまいちだった
  • echo作ってるチームがarmorというのを後から始めていて、echo今後もやっていくのか少し不安がある

という感じで、AppEngine+Go1.8で使うならあまりオススメできません。

chiよさそう

そこでechoの代わりに使えるものを探してみて、chiを知りました。

go-chi/chi: lightweight, idiomatic and composable router for building Go HTTP services

READMEにある説明によると

  • 軽量
  • 高速
  • 標準pkg以外の外部パッケージに依存しない
  • net/httpに100%準拠
  • APIをモジュール化できる仕組み(ミドルウェア、routeグループ、subrouter)

ということで、薄いpkgでルーティングをいい感じにしたい人にはぴったりです。

chiのレポジトリのREADMEにある以下のコードを見ると、できることがだいたい分かると思います。

import (
  //...
  "context"
  "github.com/go-chi/chi"
  "github.com/go-chi/chi/middleware"
)

func main() {
  r := chi.NewRouter()

  // A good base middleware stack
  r.Use(middleware.RequestID)
  r.Use(middleware.RealIP)
  r.Use(middleware.Logger)
  r.Use(middleware.Recoverer)

  // Set a timeout value on the request context (ctx), that will signal
  // through ctx.Done() that the request has timed out and further
  // processing should be stopped.
  r.Use(middleware.Timeout(60 * time.Second))

  r.Get("/", func(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("hi"))
  })

  // RESTy routes for "articles" resource
  r.Route("/articles", func(r chi.Router) {
    r.With(paginate).Get("/", listArticles)                           // GET /articles
    r.With(paginate).Get("/{month}-{day}-{year}", listArticlesByDate) // GET /articles/01-16-2017

    r.Post("/", createArticle)                                        // POST /articles
    r.Get("/search", searchArticles)                                  // GET /articles/search

    // Regexp url parameters:
    r.Get("/{articleSlug:[a-z-]+}", getArticleBySlug)                // GET /articles/home-is-toronto
    
    // Subrouters:
    r.Route("/{articleID}", func(r chi.Router) {
      r.Use(ArticleCtx)
      r.Get("/", getArticle)                                          // GET /articles/123
      r.Put("/", updateArticle)                                       // PUT /articles/123
      r.Delete("/", deleteArticle)                                    // DELETE /articles/123
    })
  })

  // Mount the admin sub-router
  r.Mount("/admin", adminRouter())

  http.ListenAndServe(":3333", r)
}

ルーティングにmiddleware入れる仕組みがあるのがいいですね。

例えば、ベーシック認証を行うmiddlewareは次のように書けます。

var userPasswords = map[string]string{
    "user": "PassW0rd",
}

func basicAuth(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        usr, pw, ok := r.BasicAuth()
        if !ok {
            w.Header().Set("WWW-Authenticate", "Basic")
            w.WriteHeader(http.StatusUnauthorized)
            http.Error(w, "auth required", http.StatusUnauthorized)
            return
        }

        if userPasswords[usr] != pw {
            http.Error(w, "incorrect auth info", http.StatusUnauthorized)
            return
        }

        next.ServeHTTP(w, r)
    })
}

で、ベーシック認証かけたいところで

    r.With(basicAuth).Get("/internal", secretPage) 

という感じで使うことが可能です。

gorilla/muxを使ったことはないけど、READMEを読む限り、書き方的にはchiが好きの方が好きです。

ということで、ドキュメントを読んでちょっと触ってみた限り、とてもいい感じがするのでオススメです。