年中アイス

いろいろつらつら

golangのHTTPサーバを構成しているもの

golangのHTTPサーバは、少量のコードで動くものを作ることができます。内部的には、net/httpパッケージのServerHandlerで構成されます。

  • Serverは名前の通り、Serveするもので、HTTPをどのネットワークソースで提供するかを定義するものです。
  • Handlerは、HTTPリクエストを実際にどう処理するかを抽象化したinterfaceで、ServeHTTP(w http.ResponseWriter req *http.Request)を提供しています。

しかし、これらは簡易に使う場合、見えなくなっています。どうしてそうなっているかを理解することで、幾つかのテクニックを知ることができます。

コードが短くなる過程

よく例として出てくるHTTPサーバの実装例は以下のコードです。

通常版

package main

import (
    "net/http"
)

func main() {
    http.HandleFunc("/index", func(w http.ResponseWriter, req *http.Request) {
        w.Write([]byte("Hello world"))
    })
    http.ListenAndServe(":3000", nil)
}

GET /indexでHello worldを返します

curl http://localhost:3000/index

Hello world

ここにはServerもHandlerも登場しません。これを省略せずに書くと、おおよそ以下のコードになります。

冗長版

package main

import (
    "net/http"
)

func main() {
    m := http.NewServeMux()
    h := new(HelloHandler)
    m.Handle("/index", h)

    s := http.Server{
        Addr:    ":3000",
        Handler: m,
    }
    s.ListenAndServe()
}

type HelloHandler struct{}

func (h *HelloHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    w.Write([]byte("Hello world"))
}

通常版と比べると長いですね。この冗長版を以下のような3つのテクニックで短く簡単な通常版にしていきます。

その1 構造体作ってServeHTTP()作る必要をなくす

冗長版では、HelloHandler構造体を定義して、そこにHandlerinterfaceであるServeHTTP(w http.ResponseWriter req *http.Request)を実装しています。

機能を作るたびにServeHTTP()のための構造体を作るのは面倒です。そこで、ServeHTTP()と同じ関数型のaliasであるHandlerFuncが用意されています。

type HandlerFunc func(ResponseWriter, *Request)

func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
    f(w, r)
}

HandlerFunc型にServeHTTP()を作り、内部的に自身の関数を呼び出しています。

これによって、2つのメリットが生まれています。

  • わざわざ構造体を定義しなくても良くなった
  • ServeHTTPという関数名に縛られなくなった

それをコードに適用すると

package main

import (
    "net/http"
)

func main() {
    m := http.NewServeMux()

    // HandlerFunc型にキャストして渡す
    m.Handle("/index", http.HandlerFunc(index))

    s := http.Server{
        Addr:    ":3000",
        Handler: m,
    }
    s.ListenAndServe()
}

// ただの関数に
func index(w http.ResponseWriter, req *http.Request) {
    w.Write([]byte("Hello world"))
}

func(http.ResponseWriter, *http.Request)という関数を好きな名前で作り、HandlerFuncにキャストするだけで良くなります。さらに、このキャスト自体も意識しなくても良いです。詳しくは次の節で。

その2 ServeMuxは使うので予め用意する

ServeMuxは、HTTPリクエストに対しての処理をPathごとに登録することができる構造体です。ServeMuxを使わなくても、HTTPリクエストを処理することはできますが、Pathごとの振り分けは余程のことがない限り必ず使うため、省略できるようになっています。

冗長版では、http.NewServeMux()して、PathのパターンとHelloHandler構造体を渡していました。これを短くするために、net/httpは予めパッケージの変数にDefaultServeMuxという変数を持っています。DefaultServeMuxに対する登録を行うことができる関数も用意されていて、そのうちの一つがhttp.HandleFunc(pattern string, func(http.ResponseWriter, *http.Request))です。第一引数のpatternでPathを、そして第二引数はHandlerFuncにキャスト可能な関数型です。前述のHandlerFuncへのキャストを意識しなくて良い理由は、このhttp.HandleFunc()が内部でキャストしてくれるからです。

これによって2つのメリットが生まれています。

  • ServeMuxを自分でnewしなくて良くなった
  • HandlerFuncにキャストせずに、Pathと決まった引数の関数を実装して渡すだけで良くなった

再びコードに適用します。

package main

import (
    "net/http"
)

func main() {
    // ServeMuxは作らず、HandlerFuncへのキャストもしなくていい
    http.HandleFunc("/index", index)

    s := http.Server{
        Addr: ":3000",
        // packageで用意されているDefaultServeMuxを使う
        Handler: http.DefaultServeMux,
    }
    s.ListenAndServe()
}

func index(w http.ResponseWriter, req *http.Request) {
    w.Write([]byte("Hello world"))
}

また少し短くなりました。

その3 Serverも必ず使うので、1発に

Serverは、HTTPをどうServeするかを定義します。基本的にはどのネットワークソースでHTTPを提供するのかと、そのHTTPをどう処理するかのHandlerがあれば動きます。

冗長版では、Serverをnewしてport 3000番をAddrとHandlerを入れて、ListenAndServe()しています。多くの場合、これだけで済むので、その2つだけで渡せば、内部的にServerを動かしてくれるhttp.ListenAndServe(adds string, handler Handler) errorが提供されています。

func ListenAndServe(addr string, handler Handler) error {
    server := &Server{Addr: addr, Handler: handler}
    return server.ListenAndServe()
}

それを適用(+無名関数化)すると、最初に書いた通常版のコードになりました。

package main

import (
    "net/http"
)

func main() {
    // index()は無名関数に
    http.HandleFunc("/index", func(w http.ResponseWriter, req *http.Request) {
        w.Write([]byte("Hello world"))
    })
    // Serverは作らなくても、DefaultServeMuxで起動
    http.ListenAndServe(":3000", nil)
}

通常版では、http.ListenAndServe(adds string, handler Handler) errorの第一引数に、port番号を表す文字列を渡し、第二引数はnilです。nilを渡すとServerの内部で、前述のパッケージ変数のDefaultServeMuxが使われます。*1自分でHandlerを実装して、このListenAndServe()の第二引数に入れてを呼ぶことも可能です。その場合でもServerの作成は省略できます。

ちなみに、Serverの作成を省略しないケースはというと、Serverには他にも各種Timeoutや最大リクエストサイズなど、HTTPを受け付ける上での制約を課すことができます。それを使う場合はServerを自分でnewして、対象の変数に値を入れます。ここ参照*2

まとめ

以上で、冗長版が通常版に省略されていくテクニックを知ることができました。いずれも、ほとんどやることが定型化しているのであれば、できるだけ手間が少なくなるように設計されています。逆に、細かくやりたい場合は、省略せずに書くことで実現できます。

ライブラリを書いたりすると、このバランスが難しかったりしますが、net/httpパッケージで使われているやり方は参考になります。

参考

*1:その2でServerにhttp.DefaultServeMuxを渡していますが、Addrだけで動きます(Handlerは指定しなかったらnilなので)

*2:多くの場合、そういう制約は前段にnginxなどを置いて対応するかと思うので、省略形が提供されているのかなと思います。内部のAPIなどでnginxを前段に置くのは手間だけど、制限したい場合などはこれを使います。