Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 13716] ファイルの概要

このコミットは、Go言語の標準ライブラリである net/http パッケージの ServeMuxHandler メソッドを追加するものです。これにより、ServeMux のリクエストディスパッチロジックが、リクエストに追加の制約を課すラッパー(ミドルウェアなど)から利用可能になります。

コミット

commit e29659b3c30a79cdbd61ac6b68d5cead57ef2de7
Author: Russ Cox <rsc@golang.org>
Date:   Fri Aug 31 12:16:31 2012 -0400

    net/http: add (*ServeMux).Handler method
    
    The Handler method makes the ServeMux dispatch logic
    available to wrappers that enforce additional constraints
    on requests.
    
    R=golang-dev, bradfitz, dsymonds
    CC=golang-dev
    https://golang.org/cl/6450165

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/e29659b3c30a79cdbd61ac6b68d5cead57ef2de7

元コミット内容

net/http: add (*ServeMux).Handler method

Handler メソッドは、ServeMux のディスパッチロジックを、リクエストに追加の制約を課すラッパーで利用できるようにします。

変更の背景

Goの net/http パッケージは、ウェブアプリケーションを構築するための基本的なHTTP機能を提供します。その中心的なコンポーネントの一つが ServeMux であり、これはHTTPリクエストのURLパスに基づいて適切な Handler を選択し、ディスパッチする役割を担います。

このコミットが導入される以前は、ServeMux がリクエストをどのように処理し、どのハンドラにディスパッチするかという内部ロジックは、主に ServeHTTP メソッド内にカプセル化されていました。これは通常の使用シナリオでは問題ありませんが、特定の高度なユースケース、例えばリクエストが実際にハンドラに到達する前に、追加のロジック(認証、ロギング、レート制限など)を適用したい場合、直接的な介入が困難でした。

このような「ラッパー」や「ミドルウェア」の概念を実装するためには、ServeMux が最終的にどのハンドラを選択するのかを知る必要があります。しかし、これまでのAPIでは、ServeMux が内部的に決定したハンドラを外部から取得する直接的な手段がありませんでした。このため、開発者は ServeMux の内部ロジックを模倣するか、より複雑な方法でミドルウェアを実装する必要がありました。

このコミットは、ServeMux のディスパッチロジックを外部に公開する Handler メソッドを導入することで、この課題を解決します。これにより、開発者は ServeMux が選択するハンドラと、そのハンドラが登録されたパターンを事前に取得し、その上で独自のラッパーロジックを適用できるようになります。これは、より柔軟で強力なHTTPリクエスト処理パイプラインを構築するための重要な改善です。

前提知識の解説

このコミットを理解するためには、以下のGo言語の net/http パッケージに関する基本的な概念を理解しておく必要があります。

  1. http.Handler インターフェース: GoのHTTPハンドラは、http.Handler インターフェースを実装する任意の型です。このインターフェースは、単一のメソッド ServeHTTP(ResponseWriter, *Request) を持ちます。このメソッドは、HTTPリクエストを処理し、HTTPレスポンスを生成する責任を負います。

  2. http.ServeMux: ServeMux は、HTTPリクエストのURLパスに基づいて、登録された http.Handler をディスパッチするHTTPリクエストマルチプレクサ(ルーター)です。Handle メソッドを使用して、特定のパターン(例: /users/, /api/v1/)とそれに対応するハンドラを登録します。ServeMuxhttp.Handler インターフェースも実装しているため、http.ListenAndServe 関数に直接渡すことができます。

  3. リクエストの正規化とリダイレクト: net/http パッケージは、URLパスの正規化を自動的に行います。例えば、/dir のようなパスが登録されている場合、/dir/ へのリクエストは自動的に /dir にリダイレクトされることがあります。また、... を含むパスも正規化されます。これは、URLのセマンティクスを統一し、重複したコンテンツを避けるために重要です。

  4. ミドルウェア (Middleware): ミドルウェアは、HTTPリクエストとレスポンスの処理パイプラインに介入し、追加の機能(認証、ロギング、エラーハンドリングなど)を提供するソフトウェアコンポーネントです。Goの net/http におけるミドルウェアは通常、http.Handler を引数に取り、新しい http.Handler を返す関数として実装されます。これにより、複数のミドルウェアをチェーンのように連結して適用することができます。

    func LoggingMiddleware(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            log.Printf("Request: %s %s", r.Method, r.URL.Path)
            next.ServeHTTP(w, r) // 次のハンドラを呼び出す
        })
    }
    

    このコミットが導入する Handler メソッドは、このようなミドルウェアが ServeMux のディスパッチロジックをより深く統合し、特定のパスにマッチするハンドラを事前に知ることを可能にします。

技術的詳細

このコミットの主要な変更点は、http.ServeMuxHandler(r *Request) (h Handler, pattern string) メソッドが追加されたことです。このメソッドは、与えられたHTTPリクエスト r に対して ServeMux が最終的にディスパッチする http.Handler と、そのハンドラが登録されているパターン文字列を返します。

変更の具体的な内容は以下の通りです。

  1. muxEntry 構造体の変更: ServeMux の内部で使用される muxEntry 構造体に pattern フィールドが追加されました。

    type muxEntry struct {
        explicit bool
        h        Handler
        pattern  string // 新しく追加されたフィールド
    }
    

    これにより、ServeMux が内部で管理する各ハンドラエントリに、そのハンドラが登録された元のパターンを紐付けて保持できるようになります。

  2. (*ServeMux).match メソッドの変更: match メソッドは、与えられたパスに最も特異的にマッチするハンドラを検索する内部ヘルパー関数です。このメソッドのシグネチャが変更され、マッチしたハンドラだけでなく、そのハンドラが登録されたパターンも返すようになりました。

    // 変更前: func (mux *ServeMux) match(path string) Handler
    // 変更後: func (mux *ServeMux) match(path string) (h Handler, pattern string)
    

    これにより、match の呼び出し元は、ディスパッチされるハンドラと、それに紐づくパターン情報の両方を取得できます。

  3. (*ServeMux).Handler メソッドの追加: これがこのコミットの核心です。新しい Handler メソッドは、外部から ServeMux のディスパッチロジックを利用するための公開APIです。

    func (mux *ServeMux) Handler(r *Request) (h Handler, pattern string) {
        // ...
    }
    

    このメソッドは以下のロジックを実行します。

    • パスの正規化とリダイレクトの処理: r.Method != "CONNECT" の場合、r.URL.PathcleanPath 関数で正規化します。もし正規化されたパスが元のパスと異なる場合、RedirectHandler を使用して永続的なリダイレクト(HTTP 301 Moved Permanently)を行うハンドラを返します。この際、リダイレクト先のパスにマッチするパターンも取得します。
    • 実際のハンドラディスパッチ: 正規化されたパス(またはCONNECTメソッドの場合は元のパス)とリクエストのホストに基づいて、内部の mux.handler メソッドを呼び出し、最終的なハンドラとパターンを取得します。
  4. (*ServeMux).handler メソッドの変更: handler メソッドは、ServeMux.Handler の内部実装であり、パスがすでに正規化されていることを前提としています。このメソッドも match と同様に、ハンドラとパターンを返すようにシグネチャが変更されました。

    // 変更前: func (mux *ServeMux) handler(r *Request) (h Handler)
    // 変更後: func (mux *ServeMux) handler(host, path string) (h Handler, pattern string)
    

    また、NotFoundHandler() を返す場合も、空のパターン文字列 "" を返すように変更されました。

  5. (*ServeMux).ServeHTTP メソッドの変更: ServeMuxhttp.Handler インターフェースを実装するために提供する ServeHTTP メソッドは、新しい Handler メソッドを利用するように変更されました。

    // 変更前:
    // if r.Method != "CONNECT" {
    //     if p := cleanPath(r.URL.Path); p != r.URL.Path {
    //         w.Header().Set("Location", p)
    //         w.WriteHeader(StatusMovedPermanently)
    //         return
    //     }
    // }
    // mux.handler(r).ServeHTTP(w, r)
    
    // 変更後:
    // h, _ := mux.Handler(r)
    // h.ServeHTTP(w, r)
    

    これにより、ServeHTTP の内部にあったパスの正規化とリダイレクトのロジックが Handler メソッドに集約され、コードの重複が解消され、よりクリーンになりました。

  6. (*ServeMux).Handle メソッドの変更: Handle メソッドは、パターンとハンドラを ServeMux に登録する際に、muxEntry にパターン文字列も保存するように変更されました。これにより、match メソッドがパターンを返すことができるようになります。

これらの変更により、ServeMux のディスパッチロジックがよりモジュール化され、外部からその結果(どのハンドラが選択され、どのパターンにマッチしたか)を事前に取得できるようになりました。これは、ミドルウェアやカスタムルーティングロジックを実装する際に非常に有用です。

コアとなるコードの変更箇所

このコミットで変更された主要なファイルは以下の2つです。

  1. src/pkg/net/http/server.go:

    • muxEntry 構造体に pattern string フィールドが追加。
    • (*ServeMux).match メソッドの戻り値に pattern string が追加。
    • (*ServeMux).Handler メソッドが新規追加。
    • (*ServeMux).handler メソッドのシグネチャと実装が変更され、pattern string を返すように。
    • (*ServeMux).ServeHTTP メソッドが、新しく追加された Handler メソッドを利用するように変更。
    • (*ServeMux).Handle メソッドが、muxEntrypattern を保存するように変更。
  2. src/pkg/net/http/server_test.go:

    • TestServeMuxHandler という新しいテスト関数が追加され、(*ServeMux).Handler メソッドの動作を検証。
    • serveMuxRegisterserveMuxTests というテストデータが定義され、様々なリクエストパスと期待されるハンドラ、パターン、HTTPステータスコードの組み合わせをテスト。
    • テスト用のヘルパー関数 servecodeSaver が追加。

コアとなるコードの解説

src/pkg/net/http/server.go の変更点

// muxEntry 構造体に pattern フィールドを追加
type muxEntry struct {
	explicit bool
	h        Handler
	pattern  string // 新規追加
}

// match メソッドがハンドラとパターンを返すように変更
// 最も特異的な(最長の)パターンが優先される
func (mux *ServeMux) match(path string) (h Handler, pattern string) {
	var n = 0
	for k, v := range mux.m {
		if !pathMatch(k, path) {
			continue
		}
		if h == nil || len(k) > n {
			n = len(k)
			h = v.h
			pattern = v.pattern // マッチしたエントリのパターンを返す
		}
	}
	return
}

// 新しく追加された (*ServeMux).Handler メソッド
// リクエスト r に使用するハンドラを返す
// r.Method, r.Host, r.URL.Path を参照する
// 常に nil でないハンドラを返す
// パスが正規形でない場合、正規パスにリダイレクトする内部生成ハンドラを返す
// また、リクエストにマッチする登録済みパターン、または内部生成リダイレクトの場合はリダイレクト後にマッチするパターンを返す
// リクエストに適用される登録済みハンドラがない場合、"page not found" ハンドラと空のパターンを返す
func (mux *ServeMux) Handler(r *Request) (h Handler, pattern string) {
	if r.Method != "CONNECT" {
		if p := cleanPath(r.URL.Path); p != r.URL.Path {
			// パスが正規形でない場合、リダイレクトハンドラを返す
			// リダイレクト先のパスにマッチするパターンも取得
			_, pattern = mux.handler(r.Host, p)
			return RedirectHandler(p, StatusMovedPermanently), pattern
		}
	}
	// 正規化されたパス(またはCONNECTメソッドの場合は元のパス)で内部ハンドラを呼び出す
	return mux.handler(r.Host, r.URL.Path)
}

// handler メソッドのシグネチャと実装が変更され、ハンドラとパターンを返すように
// パスは正規形であることが前提(CONNECTメソッドを除く)
func (mux *ServeMux) handler(host, path string) (h Handler, pattern string) {
	mux.mu.RLock()
	defer mux.mu.RUnlock()

	// ホスト固有のパターンが汎用パターンよりも優先される
	if mux.hosts {
		h, pattern = mux.match(host + path)
	}
	if h == nil {
		h, pattern = mux.match(path)
	}
	if h == nil {
		// マッチするハンドラがない場合、NotFoundHandler と空のパターンを返す
		h, pattern = NotFoundHandler(), ""
	}
	return
}

// ServeHTTP メソッドが新しい Handler メソッドを利用するように変更
func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) {
	h, _ := mux.Handler(r) // Handler メソッドを呼び出してハンドラを取得
	h.ServeHTTP(w, r)      // 取得したハンドラでリクエストを処理
}

// Handle メソッドが muxEntry にパターンを保存するように変更
func (mux *ServeMux) Handle(pattern string, handler Handler) {
	// ...
	mux.m[pattern] = muxEntry{explicit: true, h: handler, pattern: pattern} // pattern を保存
	// ...
	// リダイレクトハンドラを登録する場合も pattern を保存
	// mux.m[pattern[0:n-1]] = muxEntry{h: RedirectHandler(path, StatusMovedPermanently), pattern: pattern}
}

src/pkg/net/http/server_test.go の新規追加

このファイルは、ServeMux.Handler メソッドの動作を検証するためのテストケースを含んでいます。

// Copyright 2012 The Go Authors.  All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package http

import (
	"net/url"
	"testing"
)

// テスト用のハンドラ登録データ
var serveMuxRegister = []struct {
	pattern string
	h       Handler
}{
	{"/dir/", serve(200)},
	{"/search", serve(201)},
	{"codesearch.google.com/search", serve(202)},
	{"codesearch.google.com/", serve(203)},
}

// 指定されたHTTPステータスコードを返すハンドラ関数を生成
func serve(code int) HandlerFunc {
	return func(w ResponseWriter, r *Request) {
		w.WriteHeader(code)
	}
}

// ServeMux.Handler メソッドのテストケース
var serveMuxTests = []struct {
	method  string
	host    string
	path    string
	code    int    // 期待されるHTTPステータスコード
	pattern string // 期待されるマッチパターン
}{
	// 様々なリクエストパスと期待される結果の組み合わせ
	{"GET", "google.com", "/", 404, ""},
	{"GET", "google.com", "/dir", 301, "/dir/"}, // リダイレクトのテスト
	{"GET", "google.com", "/dir/", 200, "/dir/"},
	{"GET", "google.com", "/dir/file", 200, "/dir/"},
	{"GET", "google.com", "/search", 201, "/search"},
	// ... 他のテストケース ...
	{"CONNECT", "google.com", "/dir", 301, "/dir/"}, // CONNECT メソッドのリダイレクトテスト
	{"CONNECT", "google.com", "/../search", 404, ""}, // CONNECT メソッドのパス正規化テスト
	// ...
}

// TestServeMuxHandler は ServeMux.Handler メソッドをテストする
func TestServeMuxHandler(t *testing.T) {
	mux := NewServeMux()
	for _, e := range serveMuxRegister {
		mux.Handle(e.pattern, e.h) // ハンドラを登録
	}

	for _, tt := range serveMuxTests {
		r := &Request{
			Method: tt.method,
			Host:   tt.host,
			URL: &url.URL{
				Path: tt.path,
			},
		}
		h, pattern := mux.Handler(r) // テスト対象の Handler メソッドを呼び出す
		cs := &codeSaver{h: Header{}}
		h.ServeHTTP(cs, r) // 取得したハンドラを実行し、ステータスコードを保存
		if pattern != tt.pattern || cs.code != tt.code {
			t.Errorf("%s %s %s = %d, %q, want %d, %q", tt.method, tt.host, tt.path, cs.code, pattern, tt.code, tt.pattern)
		}
	}
}

// codeSaver は WriteHeader に渡されたコードを保存する ResponseWriter
type codeSaver struct {
	h    Header
	code int
}

func (cs *codeSaver) Header() Header              { return cs.h }
func (cs *codeSaver) Write(p []byte) (int, error) { return len(p), nil }
func (cs *codeSaver) WriteHeader(code int)        { cs.code = code } // コードを保存

このテストコードは、様々なHTTPメソッド、ホスト、パスの組み合わせに対して ServeMux.Handler が期待通りにハンドラとパターンを返すか、そしてそのハンドラが正しいHTTPステータスコードを生成するかを網羅的に検証しています。特に、パスの正規化とリダイレクトの挙動、およびホスト名に基づくルーティングが正しく機能していることを確認しています。

関連リンク

  • Go言語 net/http パッケージのドキュメント: https://pkg.go.dev/net/http
  • Go言語のHTTPミドルウェアに関する一般的な議論: (Web検索で「Go http middleware」などで検索すると多くの記事が見つかります)

参考にした情報源リンク