[インデックス 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.Handle
や http.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.HandleFunc
やhttp.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
ファイルに集中しています。
-
ServeMux
構造体の変更:- 以前は
m map[string]Handler
というシンプルなマップを持っていました。 - 変更後、
m map[string]muxEntry
となり、muxEntry
という新しい構造体を値として持つようになりました。 sync.RWMutex
型のmu
フィールドが追加され、マップへの並行アクセスを保護するようになりました。
- 以前は
-
muxEntry
構造体の導入:type muxEntry struct { explicit bool h Handler }
explicit
(bool): そのエントリが明示的に登録されたもの(Handle
またはHandleFunc
呼び出しによるもの)であるか、それとも暗黙的に生成されたリダイレクトハンドラであるかを示すフラグです。h
(Handler
): 実際にリクエストを処理するhttp.Handler
インスタンスです。
-
NewServeMux
関数の変更:ServeMux
のm
フィールドがmuxEntry
を持つマップとして初期化されるようになりました。
-
ServeHTTP
メソッドの変更:ServeMux
のServeHTTP
メソッドは、リクエストを処理するハンドラを見つけるロジックをhandler
メソッドに委譲するようになりました。これにより、コードの関心事が分離され、可読性が向上しています。
-
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 }
- この新しいメソッドは、
ServeMux
のm
マップへの読み取りアクセスをRLock
/RUnlock
で保護します。 - リクエストのホストとパスに基づいて最適なハンドラを検索し、見つからない場合は
NotFoundHandler
を返します。
- この新しいメソッドは、
-
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
メソッドは、ServeMux
のm
マップへの書き込みアクセスをLock
/Unlock
で保護します。- 重複登録の検出:
if mux.m[pattern].explicit
という条件が追加されました。これは、指定されたpattern
が既にServeMux
に明示的に登録されているかどうかをチェックします。もしexplicit
がtrue
であれば、それは既に明示的なハンドラが登録されていることを意味し、panic("http: multiple registrations for " + pattern)
を発生させます。 - 暗黙的なリダイレクトの扱い:
/path/
のような末尾スラッシュ付きのパターンが登録された場合、/path
のような末尾スラッシュなしのパターンへの暗黙的なリダイレクトハンドラが自動的に登録されます。この変更では、この暗黙的なリダイレクトが、明示的に登録されたハンドラによって上書きされないように (!mux.m[pattern[0:n-1]].explicit
) 条件が追加されました。
-
ドキュメントの更新:
doc/go1.html
とdoc/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
の変更
ServeMux
のm
フィールドが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
のゼロ値(explicit
はfalse
)が返されます。- もし
mux.m[pattern].explicit
がtrue
であれば、それは既にそのパターンに対して明示的なハンドラが登録されていることを意味します。この場合、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言語の
panic
とrecover
に関する公式ドキュメントやチュートリアル - Goの
ServeMux
における重複登録パニックに関する一般的な解説記事 (例: https://www.geeksforgeeks.org/go-net-http-servemux-duplicate-registration-panic/)