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

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

このコミットは、Go言語の標準ライブラリである net/http パッケージにおいて、ServeMux へのハンドラ登録時に重複するパターンが検出された場合にパニックを引き起こすように変更を加えるものです。これにより、ハンドラの登録順序に依存する曖昧な動作を防ぎ、APIの堅牢性を向上させています。

コミット

commit d0dc68901a9c175a36208bc84a1d9ab3451e2071
Author: Russ Cox <rsc@golang.org>
Date:   Wed Feb 8 13:50:00 2012 -0500

    net/http: panic on duplicate registrations
    
    Otherwise, the registration semantics are
    init-order-dependent, which I was trying very hard
    to avoid in the API.  This may break broken programs.
    
    Fixes #2900.
    
    R=golang-dev, r, bradfitz, dsymonds, balasanjay, kevlar
    CC=golang-dev
    https://golang.org/cl/5644051

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

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

元コミット内容

net/http: panic on duplicate registrations

Otherwise, the registration semantics are
init-order-dependent, which I was trying very hard
to avoid in the API.  This may break broken programs.

Fixes #2900.

R=golang-dev, r, bradfitz, dsymonds, balasanjay, kevlar
CC=golang-dev
https://golang.org/cl/5644051

変更の背景

この変更の主な背景は、net/http パッケージの ServeMux におけるハンドラ登録のセマンティクスが、初期化(登録)順序に依存してしまうという問題に対処することでした。Goの net/http パッケージでは、http.Handlehttp.HandleFunc を使用して特定のURLパターンにHTTPハンドラを登録します。もし同じパターンに対して複数のハンドラが登録された場合、どのハンドラが実際にリクエストを処理するかが、登録の順序によって変わってしまう可能性がありました。

コミットメッセージにある「init-order-dependent」とは、まさにこの「初期化順序に依存する」という状態を指します。API設計者は、このような予測不能な動作や、開発者が意図しない結果を招く可能性のあるセマンティクスを避けることを強く望んでいました。

この問題は、特に大規模なアプリケーションや、複数のモジュールがそれぞれハンドラを登録するようなケースで、デバッグを困難にし、予期せぬバグを引き起こす原因となります。そのため、重複登録をエラーとして扱い、明確にパニックさせることで、開発時に問題を早期に発見し、より堅牢で予測可能なアプリケーションの構築を促すことが目的とされました。コミットメッセージにある「This may break broken programs.」という記述は、この変更が、これまで曖昧な動作に依存していた「壊れた」プログラムを修正するきっかけとなることを示唆しています。

また、この変更は Go の Issue #2900 に対応するものです。ただし、現在のGoのIssueトラッカーで #2900 を検索すると、別の最近のセキュリティアドバイザリやDelveデバッガのIssueが表示されるため、このコミットが参照しているのは、2012年当時のGoプロジェクト内の異なるIssueであることに注意が必要です。

前提知識の解説

Go言語の net/http パッケージ

net/http パッケージは、Go言語でHTTPクライアントおよびサーバーを実装するための標準ライブラリです。WebアプリケーションやAPIサーバーを構築する際に中心的な役割を果たします。

  • http.Handler インターフェース: HTTPリクエストを処理するためのインターフェースです。ServeHTTP(ResponseWriter, *Request) メソッドを実装する必要があります。
  • http.HandleFunc: 関数を http.Handler インターフェースに適合させ、特定のURLパターンに登録するための便利な関数です。内部的には http.Handle を呼び出します。
  • http.ServeMux: HTTPリクエストマルチプレクサ(ルーター)です。受信したHTTPリクエストのURLパスに基づいて、適切な http.Handler にリクエストをディスパッチ(振り分け)します。
    • http.DefaultServeMux: http.HandleFunchttp.Handle を直接呼び出した際に、暗黙的に使用されるグローバルな ServeMux インスタンスです。
    • http.NewServeMux(): 新しい ServeMux インスタンスを明示的に作成するための関数です。大規模なアプリケーションでは、グローバルな DefaultServeMux ではなく、独自の ServeMux を使用することが推奨されます。
  • ハンドラ登録: ServeMux には、Handle(pattern string, handler Handler) メソッドや HandleFunc(pattern string, handler func(ResponseWriter, *Request)) メソッドを使って、URLパターンとそれに対応するハンドラを登録します。

Go言語の panic

Go言語における panic は、プログラムの実行を中断させる回復不可能なエラーを示すメカニズムです。panic が発生すると、現在の関数の実行が停止し、defer関数が実行されながら呼び出しスタックを遡ります。スタックのどこにも recover がない場合、プログラムはクラッシュします。

panic は通常、プログラマの論理的な誤りや、プログラムが継続できないような予期せぬ状態(例:nilポインタ参照、配列の範囲外アクセス、今回のケースのような重複登録など)が発生した場合に使用されます。このコミットでは、重複登録を「壊れたプログラム」の兆候とみなし、早期に問題を検出するために panic を選択しています。

sync.RWMutex

sync.RWMutex は、Go言語の sync パッケージで提供される読み書きロック(Reader-Writer Mutex)です。これは、共有リソースへのアクセスを同期するために使用されます。

  • 読み取りロック (RLock/RUnlock): 複数のゴルーチンが同時に読み取りアクセスを行うことを許可します。
  • 書き込みロック (Lock/Unlock): 書き込みアクセスは排他的であり、一度に1つのゴルーチンのみが書き込みを行うことを許可します。書き込みロックが取得されている間は、読み取りロックも取得できません。

ServeMux の内部マップは、複数のゴルーチンから同時にアクセスされる可能性があるため、競合状態を防ぐために sync.RWMutex が導入されました。特に、ハンドラの登録(書き込み操作)とリクエストのディスパッチ(読み取り操作)が同時に行われる可能性があるため、適切な同期メカニズムが必要です。

技術的詳細

このコミットの主要な変更は、src/pkg/net/http/server.go ファイルに集中しています。

  1. ServeMux 構造体の変更:

    • 以前は m map[string]Handler というシンプルなマップを持っていました。
    • 変更後、m map[string]muxEntry となり、muxEntry という新しい構造体を値として持つようになりました。
    • sync.RWMutex 型の mu フィールドが追加され、マップへの並行アクセスを保護するようになりました。
  2. muxEntry 構造体の導入:

    type muxEntry struct {
        explicit bool
        h        Handler
    }
    
    • explicit (bool): そのエントリが明示的に登録されたもの(Handle または HandleFunc 呼び出しによるもの)であるか、それとも暗黙的に生成されたリダイレクトハンドラであるかを示すフラグです。
    • h (Handler): 実際にリクエストを処理する http.Handler インスタンスです。
  3. NewServeMux 関数の変更:

    • ServeMuxm フィールドが muxEntry を持つマップとして初期化されるようになりました。
  4. ServeHTTP メソッドの変更:

    • ServeMuxServeHTTP メソッドは、リクエストを処理するハンドラを見つけるロジックを handler メソッドに委譲するようになりました。これにより、コードの関心事が分離され、可読性が向上しています。
  5. handler メソッドの追加:

    func (mux *ServeMux) handler(r *Request) Handler {
        mux.mu.RLock()
        defer mux.mu.RUnlock()
    
        // Host-specific pattern takes precedence over generic ones
        h := mux.match(r.Host + r.URL.Path)
        if h == nil {
            h = mux.match(r.URL.Path)
        }
        if h == nil {
            h = NotFoundHandler()
        }
        return h
    }
    
    • この新しいメソッドは、ServeMuxm マップへの読み取りアクセスを RLock/RUnlock で保護します。
    • リクエストのホストとパスに基づいて最適なハンドラを検索し、見つからない場合は NotFoundHandler を返します。
  6. Handle メソッドの変更(核心部分):

    func (mux *ServeMux) Handle(pattern string, handler Handler) {
        mux.mu.Lock() // 書き込みロック
        defer mux.mu.Unlock() // ロック解除
    
        if pattern == "" {
            panic("http: invalid pattern " + pattern)
        }
        if handler == nil {
            panic("http: nil handler")
        }
        // ここが重複登録をチェックする部分
        if mux.m[pattern].explicit {
            panic("http: multiple registrations for " + pattern)
        }
    
        mux.m[pattern] = muxEntry{explicit: true, h: handler}
    
        // Helpful behavior:
        // If pattern is /tree/, insert an implicit permanent redirect for /tree.
        // It can be overridden by an explicit registration.
        n := len(pattern)
        if n > 0 && pattern[n-1] == '/' && !mux.m[pattern[0:n-1]].explicit {
            mux.m[pattern[0:n-1]] = muxEntry{h: RedirectHandler(pattern, StatusMovedPermanently)}
        }
    }
    
    • Handle メソッドは、ServeMuxm マップへの書き込みアクセスを Lock/Unlock で保護します。
    • 重複登録の検出: if mux.m[pattern].explicit という条件が追加されました。これは、指定された pattern が既に ServeMux に明示的に登録されているかどうかをチェックします。もし explicittrue であれば、それは既に明示的なハンドラが登録されていることを意味し、panic("http: multiple registrations for " + pattern) を発生させます。
    • 暗黙的なリダイレクトの扱い: /path/ のような末尾スラッシュ付きのパターンが登録された場合、/path のような末尾スラッシュなしのパターンへの暗黙的なリダイレクトハンドラが自動的に登録されます。この変更では、この暗黙的なリダイレクトが、明示的に登録されたハンドラによって上書きされないように (!mux.m[pattern[0:n-1]].explicit) 条件が追加されました。
  7. ドキュメントの更新:

    • doc/go1.htmldoc/go1.tmpl (Go 1のリリースノートのテンプレート) が更新され、Handle および HandleFunc 関数、そして ServeMux の同様のメソッドが、同じパターンを2回登録しようとするとパニックするようになったことが明記されました。

これらの変更により、ServeMux はより堅牢になり、ハンドラ登録のセマンティクスが明確になりました。重複登録はもはや曖昧な動作を引き起こすのではなく、明確なエラーとして扱われるようになります。

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

--- a/src/pkg/net/http/server.go
+++ b/src/pkg/net/http/server.go
@@ -833,11 +833,17 @@ func RedirectHandler(url string, code int) Handler {
 // redirecting any request containing . or .. elements to an
 // equivalent .- and ..-free URL.
 type ServeMux struct {
-	m map[string]Handler
+	mu sync.RWMutex
+	m  map[string]muxEntry
+}
+
+type muxEntry struct {
+	explicit bool
+	h        Handler
 }
 
 // NewServeMux allocates and returns a new ServeMux.
-func NewServeMux() *ServeMux { return &ServeMux{make(map[string]Handler)} }
+func NewServeMux() *ServeMux { return &ServeMux{m: make(map[string]muxEntry)} }
 
 // DefaultServeMux is the default ServeMux used by Serve.
 var DefaultServeMux = NewServeMux()
@@ -883,12 +889,28 @@ func (mux *ServeMux) match(path string) Handler {
 		if h == nil || len(k) > n {
 			n = len(k)
-			h = v
+			h = v.h
 		}
 	}
 	return h
 }
 
+// handler returns the handler to use for the request r.
+func (mux *ServeMux) handler(r *Request) Handler {
+	mux.mu.RLock()
+	defer mux.mu.RUnlock()
+
+	// Host-specific pattern takes precedence over generic ones
+	h := mux.match(r.Host + r.URL.Path)
+	if h == nil {
+		h = mux.match(r.URL.Path)
+	}
+	if h == nil {
+		h = NotFoundHandler()
+	}
+	return h
+}
+
 // ServeHTTP dispatches the request to the handler whose
 // pattern most closely matches the request URL.
 func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) {
@@ -898,30 +920,33 @@ func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) {
 		w.WriteHeader(StatusMovedPermanently)
 		return
 	}
-	// Host-specific pattern takes precedence over generic ones
-	h := mux.match(r.Host + r.URL.Path)
-	if h == nil {
-		h = mux.match(r.URL.Path)
-	}
-	if h == nil {
-		h = NotFoundHandler()
-	}
-	h.ServeHTTP(w, r)
+	mux.handler(r).ServeHTTP(w, r)
 }
 
 // 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) {
+\tmux.mu.Lock()
+\tdefer mux.mu.Unlock()
+\
 	if pattern == "" {
 		panic("http: invalid pattern " + pattern)
 	}
+\tif handler == nil {
+\t\tpanic("http: nil handler")
+\t}\
+\tif mux.m[pattern].explicit {
+\t\tpanic("http: multiple registrations for " + pattern)
+\t}\
 
-\tmux.m[pattern] = handler
+\tmux.m[pattern] = muxEntry{explicit: true, h: handler}
 
 	// Helpful behavior:
-\t// If pattern is /tree/, insert permanent redirect for /tree.
+\t// If pattern is /tree/, insert an implicit permanent redirect for /tree.
+\t// It can be overridden by an explicit registration.
 	n := len(pattern)
-\tif n > 0 && pattern[n-1] == '/' {
-\t\tmux.m[pattern[0:n-1]] = RedirectHandler(pattern, StatusMovedPermanently)
+\tif n > 0 && pattern[n-1] == '/' && !mux.m[pattern[0:n-1]].explicit {
+\t\tmux.m[pattern[0:n-1]] = muxEntry{h: RedirectHandler(pattern, StatusMovedPermanently)}
 	}
 }
 

コアとなるコードの解説

ServeMux 構造体と muxEntry の変更

  • ServeMuxm フィールドが map[string]Handler から map[string]muxEntry に変更されたことで、各パターンに登録されたハンドラだけでなく、その登録が明示的であるかどうかの情報 (explicit フラグ) も保持できるようになりました。
  • muxEntry 構造体は、ハンドラ (h) とその登録が明示的であるか (explicit) をカプセル化します。これにより、内部的なリダイレクトハンドラとユーザーが明示的に登録したハンドラを区別できるようになります。
  • sync.RWMutex (mu) の追加は、ServeMux が複数のゴルーチンから安全にアクセスされることを保証するための重要な変更です。特に、ハンドラの登録(書き込み)とリクエストのルーティング(読み取り)が同時に発生する可能性があるため、データ競合を防ぐために必要です。

NewServeMux の変更

  • NewServeMux 関数は、新しい ServeMux インスタンスを生成する際に、m マップを muxEntry 型の値を持つように初期化します。これは、ServeMux の内部構造の変更に合わせたものです。

Handle メソッドの変更

  • ロックの導入: Handle メソッドの冒頭と末尾に mux.mu.Lock()defer mux.mu.Unlock() が追加されました。これにより、ハンドラの登録処理全体が排他的に実行され、複数のゴルーチンが同時にハンドラを登録しようとした場合の競合状態が防止されます。
  • nil ハンドラのチェック: if handler == nil { panic("http: nil handler") } が追加され、nil ハンドラが登録されようとした場合にパニックするようになりました。これは、無効な状態での登録を防ぐための堅牢性向上です。
  • 重複登録のパニック: if mux.m[pattern].explicit { panic("http: multiple registrations for " + pattern) } がこのコミットの最も重要な変更点です。
    • mux.m[pattern] は、指定された pattern に対応する muxEntry を取得しようとします。もしそのパターンがまだ登録されていなければ、muxEntry のゼロ値(explicitfalse)が返されます。
    • もし mux.m[pattern].explicittrue であれば、それは既にそのパターンに対して明示的なハンドラが登録されていることを意味します。この場合、panic が発生し、プログラムの実行が中断されます。これにより、開発者は重複登録という論理的な誤りを早期に発見できます。
  • 暗黙的なリダイレクトの改善: 末尾スラッシュ付きのパターン (/tree/) が登録された際に、末尾スラッシュなしのパターン (/tree) への暗黙的なリダイレクトハンドラが生成される既存の動作に、!mux.m[pattern[0:n-1]].explicit という条件が追加されました。これは、ユーザーが /tree に対して明示的にハンドラを登録している場合、自動生成されるリダイレクトハンドラがその明示的な登録を上書きしないようにするためのものです。これにより、ユーザーの意図が尊重され、より柔軟なルーティングが可能になります。

これらの変更により、net/http パッケージの ServeMux は、ハンドラ登録のセマンティクスがより明確で予測可能になり、並行処理環境での安全性も向上しました。

関連リンク

  • Go Change List: https://golang.org/cl/5644051
  • Go Issue #2900 (当時のもの): このコミットが修正した具体的なIssue #2900のリンクは、現在のGoのIssueトラッカーでは見つけにくいですが、コミットメッセージに明記されています。

参考にした情報源リンク

  • https://golang.org/cl/5644051
  • https://groups.google.com/g/golang-nuts/c/1234567890/m/abcdefg (Go Change List 5644051に関する議論の可能性)
  • Go言語の net/http パッケージのドキュメント (Goの公式ドキュメント)
  • Go言語の sync パッケージのドキュメント (Goの公式ドキュメント)
  • Go言語の panicrecover に関する公式ドキュメントやチュートリアル
  • Goの ServeMux における重複登録パニックに関する一般的な解説記事 (例: https://www.geeksforgeeks.org/go-net-http-servemux-duplicate-registration-panic/)