Edited at
Go3Day 13

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

More than 1 year has passed since last update.

この記事は 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 でも使えました。