第2の人生の構築ログ

自分の好きなことをやりつつ、インカムもしっかりと。実現していく過程での記録など。読書、IT系、旅行、お金に関係する話などの話題。

Go 言語での Web アプリの実装基礎 / これがわからないと混乱する「ハンドラの正体」編

f:id:dr_taka_n:20190926230409p:plain:w300

最近自身初の Go 言語での Web アプリを書いており、Go ネタ続きます。

Go は標準ライブラリだけで十分な Web サーバ/アプリの実装が行えると言われています。外部/3rd パーティーのライブラリを使ってその依存性などに悩む必要もないと。確かに触っているとわかるのですが、必要なものが標準で用意されており、素の HTTP の仕組みを意識しながら実装できるので、とても直感的です。フレームワークなどは便利ですが、肝心な部分を隠してしまいますので、何か特別なことをやりたい時、逆にとても単純なことをやりたい時、問題に遭遇した場合、余計に時間を食ってしまうこともあり、本質のところではなく、そのお作法に悩まされることもあり、ストレスを感じることがあるのも事実だと思います。

便利なフレームワークも標準実装に被せて用意されていますので、標準的な仕組みをキチンと理解しておくことは実践的で、応用力も付けつけられるので、今のところ標準ライブラリだけで実装しています。その中で気付いた点を幾つか書いていこうと思います。

Go で HTTP Server の実装を見ているとリクエストの処理の書き方として、いろいろな書き方ができるように見え、当初、個人的に混乱してしまったのがここでした。そういう時は OSS の良いところでソースが読めますので、実際に実装を覗いてみました。

覗いてみるとわかりますが、その考え方は至ってシンプルでした。

  • Go では、HTTP のリクエストの処理は、全てハンドラ(Handler)で処理されます。

Go では、リクエストの処理の最初の受け付けは特別に実装を入れない限り、デフォルトのマルチプレクサ(http.DefaultServeMux)が処理します。マルチプレクサという言葉自体はこれまであまりWebアプリケーションの話の中では使ったことがありませんでしたが、いろいろなリクエストを受け付けてそのリクエストを正しい処理に受け渡してあげるルーティング処理を行う交通整理の担当者のようなもののようです。
実はこのマルチプレクサ自体もハンドラで、リクエストされたパスに基づいて自身に登録されたハンドラを呼び出す処理を行っています。

さて、ハンドラとは何のことでしょうか?

type Handler interface {
    ServeHTTP(ResponseWriter, *Request)
}

ハンドラ(Handler) は ServeHTTP の実装を求めるインターフェイスです。この要件を満たす型であればどの型でもハンドラになれます。

  • つまるところ、この ServeHTTP を実装することが Go での HTTP リクエストの処理となります。

ただ、この ServeHTTP を直接実装することはあまり多くないのではないでしょうか。

となると、この Handler はどう扱われているのでしょうか。

実際にサーバが起動される時のソースで追っかけてみます。

Go では、最低以下の記述だけでも Web サーバ、Web アプリケーションの実装が行えます。

package main

import (
    "fmt"
    "log"
    "net/http"
)

func hello(w http.ResponseWriter, r *http.Request) {
    _, err := fmt.Fprintln(w, "Hello, World!")
    if err != nil {
        http.Error(w, fmt.Sprintf("%s", err), http.StatusInternalServerError)
    }
}

func main() {
    http.HandleFunc("/", hello)
    port := "8080"
    log.Printf("Listening on port %s", port)
    log.Fatal(http.ListenAndServe(fmt.Sprintf(":%s", port), nil))
}
  1. ハンドラ関数の登録
    • http.HandleFunc を使って、/ のパスに hello という関数を割り当てています
  2. サーバの起動
    • http.ListenAndServe を使ってサーバを起動します
    • 因みに第2引数のハンドラにnilを指定することで、デフォルトのマルチプレクサ(http.DefaultServeMux)が使われることになります

これだけで Web サーバと Web アプリが実装できてしまうって不思議で面白いですよね。ちなみに、ここでは ServeHTTP なんて実装していません。「つまるところ、この ServeHTTP を実装することが Go での HTTP リクエストの処理となります。」は嘘だったのでしょうか。

最初から見てきます。

ハンドラ関数の登録のところですが、http.HandleFunc の中身をみてみます。

func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
    DefaultServeMux.HandleFunc(pattern, handler)
}

DefaultServeMuxHandleFunc メソッドに処理が委譲されています。このDefaultServeMuxはデフォルトのマルチプレクサなのですが、この正体は後ほど見ます。ここではこのまま処理を追っかけていきます。DefaultServeMux.HandleFunc の処理の実体です。

// HandleFunc registers the handler function for the given pattern.
func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
    if handler == nil {
        panic("http: nil handler")
    }
    mux.Handle(pattern, HandlerFunc(handler))
}

更にmux.Handle を追いかけます。mux.HandleDefaultServeMux(ServeMux)構造体のフィールド m、つまり map[string]muxEntrypattern (パス)をキーにハンドラを登録しています。先のサンプルアプリでいうと、/ のパスに hello ハンドラ関数のハンドラが登録されるということになります。

// Handle registers the handler for the given pattern.
// If a handler already exists for pattern, Handle panics.
func (mux *ServeMux) Handle(pattern string, handler Handler) {
    mux.mu.Lock()
    defer mux.mu.Unlock()

    // ...(snip)...
    e := muxEntry{h: handler, pattern: pattern}
    mux.m[pattern] = e
    if pattern[len(pattern)-1] == '/' {
        mux.es = appendSorted(mux.es, e)
    }
    // ...(snip)...

}

hello ハンドラ関数のハンドラ」という紛らわしい書き方をしたのは意味があります。ここでは、「ハンドラ」と「ハンドラ関数」は別ものとして扱っています。mux.Handle の2つ目の引数は Handler(ハンドラ) です。hello 自体はハンドラ関数で、ハンドラではありませんでした(ServeHTTPは実装していませんので) 。いつ Handler(ハンドラ) になったのでしょうか。hello ハンドラ関数をハンドラにしたのは、HandlerFunc(handler) のところでした。

// The HandlerFunc type is an adapter to allow the use of
// ordinary functions as HTTP handlers. If f is a function
// with the appropriate signature, HandlerFunc(f) is a
// Handler that calls f.
type HandlerFunc func(ResponseWriter, *Request)

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

HandlerFunc は型です。でその型に、ServeHTTP メソッドの実装が入っています。

これは何を意味しているかと言いますと、特定のシグネチャをもつ関数(ResponseWriter*Request を引数にもつ)であれば、HandlerFunc で型変換してあげると漏れなく ServerHTTP メソッドがついてきて(そのメソッド内で関数は実行されます)、ハンドラになれるということです

そうです。先のサンプルの Web アプリでは、処理を行う関数(ハンドラ関数)は定義していましたが、ServerHTTP 自体の実装はありませんでした。ですが、http.HandleFunc でそのハンドラ関数を登録することで、ハンドラ関数は ServeHTTP を持つハンドラに変換されていたのでした。

「つまるところ、この ServeHTTP を実装することが Go での HTTP リクエストの処理となります」は真実で、その通り直接ハンドラを実装し、ServeHTTPを実装することもできますし、ServeHTTPの実装を行わずとも、ResponseWriter*Request を引数にもつハンドラ関数を実装し、http.HandleFuncを使って登録することで、ハンドラを実装したことになるんですね。

いや、すっきりしました。。また、インターフェイス、構造体、メソッドはこう使うのね、と勉強にもなりました。

さて、途中飛ばしておりましたDefaultServeMuxの正体を見ておきます。何者でしょうか。

Go では、リクエストの処理は通常、デフォルトのマルチプレクサ(http.DefaultServeMux)が処理します。実体は以下のように定義されています。

// DefaultServeMux is the default ServeMux used by Serve.
var DefaultServeMux = &defaultServeMux

var defaultServeMux ServeMux

実体は ServeMux 型です。これは構造体になっています。

type ServeMux struct {
    mu    sync.RWMutex
    m     map[string]muxEntry
    es    []muxEntry // slice of entries sorted from longest to shortest.
    hosts bool       // whether any patterns contain hostnames
}

で、この構造帯のメソッドを見てみますと、ServeHTTP の実装があります。ServeMux 型は Handler インターフェイスの要件を満たす型ですので、ハンドラになりえます。

// ServeHTTP dispatches the request to the handler whose
// pattern most closely matches the request URL.
func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) {
    if r.RequestURI == "*" {
        if r.ProtoAtLeast(1, 1) {
            w.Header().Set("Connection", "close")
        }
        w.WriteHeader(StatusBadRequest)
        return
    }
    h, _ := mux.Handler(r)
    h.ServeHTTP(w, r)
}

まさにこの部分が先に記載しました 「このマルチプレクサ自体も実はハンドラで、リクエストされたパスに基づいて自身に登録されたハンドラ関数を呼び出す処理を行っています。」 の部分になります。
(mux.Handler(r) がリクエストパスに対する適切なハンドラを返しています。)

ちなみに、デフォルトが DefaultServeMux と決まる部分は以下の実装です。リクエストが実際に処理されるときには、serverHandler 構造体の ServeHTTP がまず呼ばれています。

// serverHandler delegates to either the server's Handler or
// DefaultServeMux and also handles "OPTIONS *" requests.
type serverHandler struct {
    srv *Server
}

func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) {
    handler := sh.srv.Handler
    if handler == nil {
        handler = DefaultServeMux
    }
    if req.RequestURI == "*" && req.Method == "OPTIONS" {
        handler = globalOptionsHandler{}
    }
    handler.ServeHTTP(rw, req)
}

(上記は conn 構造体の serve(ctx context.Context)メソッドの中で、serverHandler{c.server}.ServeHTTP(w, w.req) と呼び出されます。)

こういうことは知らなくても実装はできますが、知っておくと何かと役立ちます。(個人的にはエラー処理を整理するのにこの構造を知っておくことが必要でした。)


The Go gopher was designed by Renée French.