URLに応じた処理(マルチプレクサとハンドラ)

Go標準ライブラリのnet/httpパッケージを利用して、リクエストをURL毎に処理する方法を説明します。 net/httpパッケージには、Webサーバの中核をなす機能として『マルチプレクサ』と『ハンドラ』があります。 それぞれの役割は次の通りです。

  • マルチプレクサ:リクエストを受け付け、URLに対応するハンドラへ転送する
  • ハンドラ:リクエストに応じた処理をして、レスポンスを返却する
+----------+          +--------------------------------------+
|          |          |                                      |
|  Client  |          |  Server                              |
|          |          |                                      |
|          |          |  +-------------+       +----------+  |
|          |          |  |             |       |          |  |
|          | +------> |  | Multiplexer | +-+-> | Handler1 |  |
|          |          |  |             |   |   |          |  |
|          |          |  +-------------+   |   +----------+  |
|          |          |                    |                 |
|          |          |                    |                 |
|          |          |                    |   +----------+  |
|          |          |                    |   |          |  |
|          |          |                    +-> | Handler2 |  |
|          |          |                        |          |  |
|          |          |                        +----------+  |
|          |          |                                      |
+----------+          +--------------------------------------+

マルチプレクサ

マルチプレクサの使い方は2通りあります。

  • net/httpパッケージが用意したデフォルトのマルチプレクサを利用する
  • マルチプレクサを作成する

デフォルトのマルチプレクサ

次はデフォルトのマルチプレクサを利用して、URLに応じた処理をするサンプルです。

package main

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

func main() {
    // URLに対応する処理を登録
    http.HandleFunc("/", defaultRoute)
    http.HandleFunc("/route1", route1)
    http.HandleFunc("/route2", route2)

    server := http.Server{
        Addr:    ":8080",
        // デフォルトのマルチプレクサを利用する場合はnilとする
        Handler: nil,
    }
    log.Fatal(server.ListenAndServe())
}

// リクエストを受け付ける処理(ハンドラ)
func defaultRoute(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "This is default route.")
}

// リクエストを受け付ける処理(ハンドラ)
func route1(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "This is /route1.")
}

// リクエストを受け付ける処理(ハンドラ)
func route2(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "This is /route2.")
}

このプログラムのポイントは次の通りです。

  • http.HandleFunc関数を用いて、マルチプレクサにURLと対応する処理(ハンドラ)を登録する
  • http.ServerHandlerフィールドにnilを指定する

なお、http.HandleFunc関数のURL指定は末尾が/かどうかで振る舞いが変わります。 末尾が/でない場合、URLが完全一致した場合にリクエストが転送されます。 末尾が/である場合、そのURLを前方一致した場合にリクエストが転送されます。 なお、/route(末尾スラッシュなし)へのアクセスは/route/へ301リダイレクトされます。

たとえば、http.HandleFunc("/route/", handler)の動作は次の通りです。

  • /route/route/へ301リダイレクト
  • /route/ → ハンドラへ転送
  • /route/a → ハンドラへ転送

Webサーバを起動して、Webブラウザで次のURLにアクセスし、URL毎に表示が変わることを確認します。

  • http://localhost:8080/
  • http://localhost:8080/route1
  • http://localhost:8080/route2

マルチプレクサの作成

次は明示的にマルチプレクサを作成するサンプルです。 デフォルトのマルチプレクサで示したサンプルと同等の処理です。

package main

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

func main() {
    // マルチプレクサを作成
    mux := http.NewServeMux()

    // URLに対応する処理を登録
    mux.HandleFunc("/", defaultRoute)
    mux.HandleFunc("/route1", route1)
    mux.HandleFunc("/route2", route2)

    server := http.Server{
        Addr:    ":8080",
        // 作成したマルチプレクサを指定
        Handler: mux,
    }
    log.Fatal(server.ListenAndServe())
}

// リクエストを受け付ける処理(ハンドラ)
func defaultRoute(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "This is default route.")
}

// リクエストを受け付ける処理(ハンドラ)
func route1(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "This is /route1.")
}

// リクエストを受け付ける処理(ハンドラ)
func route2(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "This is /route2.")
}

この例では、デフォルトのマルチプレクサではなく、自分で作成したmuxを使っています。 違いは次の3点です。

  • http.NewServeMuxでマルチプレクサを作成する
  • http.HandleFuncではなく、mux.HandleFuncでそのマルチプレクサにハンドラを登録する
  • http.ServerHandlerフィールドにnilではなくmuxを指定する

つまり、http.Serverごとに、どのマルチプレクサを使うかを明示できます。 たとえば、外部公開用のサーバには通常のAPI用マルチプレクサを渡し、管理用のサーバにはメトリクスや管理画面用のマルチプレクサを渡せます。 また、標準のServeMuxでは表現しにくい振り分けが必要になったときも、http.Serverに渡すHandlerを変えるだけで、サードパーティ製や自作のマルチプレクサに切り替えられます。

Go 1.22以降のルーティングパターン

Go 1.22以降: http.ServeMuxはHTTPメソッドとワイルドカードを含むパターンに対応しています。 動的パスの値はRequest.PathValueメソッドで取得できます。

Go 1.22以降では、標準ライブラリだけでHTTPメソッドと動的パスを組み合わせたルーティングを書けます。

package main

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

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

    mux.HandleFunc("GET /tasks", getTasks)
    mux.HandleFunc("POST /tasks", postTask)
    mux.HandleFunc("GET /tasks/{id}", getTask)

    server := http.Server{
        Addr:    ":8080",
        Handler: mux,
    }
    log.Fatal(server.ListenAndServe())
}

func getTasks(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintln(w, "tasks")
}

func postTask(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintln(w, "created task")
}

func getTask(w http.ResponseWriter, r *http.Request) {
    id := r.PathValue("id")
    fmt.Fprintf(w, "task id: %s", id)
}

Go 1.21以前のhttp.ServeMuxは、HTTPメソッドや{id}形式のワイルドカードを標準では扱いません。 その場合は、パスを自分で解析するか、サードパーティのルータや自作のマルチプレクサを利用します。

ハンドラ

ハンドラの作成方法は2通りあります。

  • ハンドラ(http.Handler型)を実装する
  • ハンドラ関数(http.HandlerFunc型)を実装する

次の2つはハンドラと呼ばれるので、どちらを指すかは文脈で判断してください。

  • ハンドラ(http.Handler型)とハンドラ関数(http.HandlerFunc型)の総称
  • ハンドラ(http.Handler型)

ハンドラ(http.Handler型)

ハンドラ(http.Handler型)はServeHTTPメソッドを持つインターフェースです。 引数はリクエストの読み込みとレスポンスの書き込みに使います。

  • 第一引数:HTTPレスポンスを書き込むための情報
  • 第二引数:HTTPリクエストを読み取るための情報

作成したハンドラをマルチプレクサに登録するため、 デフォルトのマルチプレクサではhttp.Handle関数を使います。 マルチプレクサを作成した場合はHandleメソッドを使います。

package main

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

func main() {
    // ハンドラを登録
    http.Handle("/", handlerA{})
    server := http.Server{
        Addr:    ":8080",
        Handler: nil,
    }
    log.Fatal(server.ListenAndServe())
}

type handlerA struct{}

// ハンドラを実装
func (h handlerA) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "handlerA")
}

ハンドラ関数(http.HandlerFunc型)

ハンドラ関数(http.HandlerFunc型)はハンドラ(http.Handler型)のServeHTTPメソッドと同じシグネチャを持つ関数です。

作成したハンドラをマルチプレクサに登録するため、 デフォルトのマルチプレクサではhttp.HandleFunc関数を使います。 マルチプレクサを作成した場合はHandleFuncメソッドを使います。

package main

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

func main() {
    // ハンドラ関数を登録
    http.HandleFunc("/", handlerFuncA)
    server := http.Server{
        Addr:    ":8080",
        Handler: nil,
    }
    log.Fatal(server.ListenAndServe())
}

// ハンドラ関数
func handlerFuncA(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "handlerFuncA")
}

ハンドラを作り込むことで、リクエストの内容に応じて、動的にレスポンスを作成できます。

以降のページで、リクエストとレスポンスについて説明します。