[インデックス 14702] ファイルの概要
このコミットは、Go言語の標準ライブラリである net/http パッケージ内の server.go ファイルに対して行われた変更を記録しています。具体的には、HTTPサーバーが新しい接続を処理する serve() メソッドにおける接続クローズのロジックを簡素化することを目的としています。以前のコミット (6971049) のフォローアップとして、エラーハンドリングと接続クローズの処理がより効率的かつ堅牢になるように改善されています。
コミット
net/http: simplify serve() connection close
Followup to 6971049.
R=bradfitz
CC=golang-dev
https://golang.org/cl/6970049
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/c850d0f34a7db9f6df54c1ca99e14e19859baaa0
元コミット内容
commit c850d0f34a7db9f6df54c1ca99e14e19859baaa0
Author: Dave Cheney <dave@cheney.net>
Date: Fri Dec 21 15:14:38 2012 +1100
net/http: simplify serve() connection close
Followup to 6971049.
R=bradfitz
CC=golang-dev
https://golang.org/cl/6970049
変更の背景
このコミットは、Go言語の net/http パッケージにおけるHTTPサーバーの接続処理の改善を目的としています。コミットメッセージに「Followup to 6971049」とあるように、以前のコミット 6971049 で導入された変更に関連しています。
元の serve() メソッドでは、接続のクローズ処理が defer c.close() と、panic が発生した場合の recover ブロック内の両方で行われていました。これは冗長であり、特に panic が発生しなかった場合には c.close() が二重に呼び出される可能性がありました(ただし、c.close() は冪等であるため、直接的な問題にはならないかもしれませんが、コードの意図が不明瞭になります)。
また、panic 発生時の接続クローズ処理は、c.rwc != nil という条件で c.rwc.Close() を呼び出していました。しかし、c.rwc (read-write closer) が nil でない場合でも、接続がハイジャックされている(c.hijacked() が true)場合には、サーバー側で接続をクローズすべきではありません。ハイジャックされた接続は、アプリケーションがそのライフサイクルを完全に制御するため、サーバーが勝手にクローズすると予期せぬ動作を引き起こす可能性があります。
このコミットは、これらの問題を解決し、serve() メソッドの接続クローズロジックを簡素化し、より堅牢にすることを目的としています。具体的には、defer c.close() を削除し、panic 発生時および tls.Handshake() エラー発生時にのみ c.close() を呼び出すように変更することで、接続クローズの責任を一元化し、ハイジャックされた接続の扱いを明確にしています。
前提知識の解説
このコミットを理解するためには、以下のGo言語の概念と net/http パッケージの基本的な知識が必要です。
net/httpパッケージ: Go言語でHTTPクライアントおよびサーバーを実装するための標準ライブラリです。HTTPリクエストのルーティング、レスポンスの生成、ミドルウェアの適用など、Webアプリケーション開発に必要な機能を提供します。http.Serverとhttp.conn:http.Server: HTTPサーバー全体を管理する構造体です。リスニングアドレス、ハンドラー、タイムアウトなどの設定を持ちます。http.conn: 個々のクライアント接続を表す内部的な構造体です。この構造体が、クライアントからのリクエストを読み込み、レスポンスを書き込むための低レベルな処理を担当します。
serve()メソッド:http.conn型のメソッドで、新しいクライアント接続が確立された際に呼び出され、その接続を通じてHTTPリクエストを処理し、レスポンスを返す一連の処理を管理します。このメソッドは通常、新しいゴルーチンで実行されます。deferステートメント: Go言語のキーワードで、deferに続く関数呼び出しを、その関数がリターンする直前(またはパニックから回復する直前)に実行するようにスケジュールします。リソースの解放(ファイルクローズ、ロック解除など)によく使用されます。panicとrecover():panic: プログラムの実行を停止させるGo言語の組み込み関数です。通常、回復不可能なエラーや予期せぬ状況が発生した場合に呼び出されます。recover():panicから回復するための組み込み関数です。deferされた関数内で呼び出された場合、現在のゴルーチンで発生したpanicの値を捕捉し、そのゴルーチンの実行を継続させることができます。recover()がnil以外の値を返した場合、panicが発生したことを意味します。
tls.ConnとHandshake():tls.Conn: TLS (Transport Layer Security) プロトコルを介して通信を行うネットワーク接続を表す構造体です。HTTPS通信などで使用されます。Handshake(): TLS接続の確立プロセス(ハンドシェイク)を実行するメソッドです。クライアントとサーバー間で暗号化パラメータのネゴシエーションを行います。
c.hijacked():http.connのメソッドで、現在の接続がHTTPサーバーによって「ハイジャック」されているかどうかを示します。接続がハイジャックされると、サーバーはそれ以降の接続の管理(クローズなど)を行わず、アプリケーションが直接TCP接続を制御するようになります。これはWebSocketのようなプロトコルでよく使用されます。
技術的詳細
このコミットの技術的な核心は、net/http/server.go 内の (*conn).serve() メソッドにおける接続クローズロジックの変更にあります。
変更前は、serve() メソッドの冒頭に以下の defer ステートメントがありました。
defer func() {
err := recover()
if err == nil {
return
}
const size = 4096
buf := make([]byte, size)
buf = buf[:runtime.Stack(buf, false)]
log.Printf("http: panic serving %v: %v\n%s", c.remoteAddr, err, buf)
if c.rwc != nil { // may be nil if connection hijacked
c.rwc.Close()
}
}()
defer c.close() // ここが削除される
このコードでは、defer c.close() が常に接続の最後に呼び出されるようにスケジュールされていました。また、panic が発生した場合に備えて recover() を含む defer ブロックがあり、その中で c.rwc.Close() が呼び出されていました。
変更後のコードは以下のようになります。
defer func() {
if err := recover(); err != nil { // recover() の結果を直接評価
const size = 4096
buf := make([]byte, size)
buf = buf[:runtime.Stack(buf, false)]
log.Printf("http: panic serving %v: %v\n%s", c.remoteAddr, err, buf)
}
if !c.hijacked() { // ハイジャックされていない場合のみ c.close() を呼び出す
c.close()
}
}()
// defer c.close() は削除された
// ... (中略) ...
if tlsConn, ok := c.rwc.(*tls.Conn); ok {
if err := tlsConn.Handshake(); err != nil {
// c.close() は削除された
return
}
// ...
}
主な変更点は以下の通りです。
defer c.close()の削除:serve()メソッドの冒頭にあったdefer c.close()が削除されました。これにより、接続クローズの責任がpanicハンドリングのdeferブロックに一元化されます。panicハンドリングの簡素化と改善:recover()の結果を直接ifステートメントで評価するように変更され、コードがより簡潔になりました。panicが発生した場合の接続クローズロジックがif !c.hijacked() { c.close() }に変更されました。これにより、接続がハイジャックされている場合にはサーバーが接続をクローズしないという、より正確な動作が保証されます。以前はc.rwc != nilという条件でしたが、これはハイジャックされた接続でもc.rwcがnilでない場合があるため、不正確でした。
tls.Handshake()エラー時のc.close()削除:tls.Handshake()がエラーを返した場合のc.close()呼び出しが削除されました。これは、panicハンドリングのdeferブロックが最終的にc.close()を呼び出すため、ここでの明示的なクローズは不要になったためです。
これらの変更により、serve() メソッドは以下の点で改善されました。
- 簡潔性: 接続クローズのロジックが重複なく、一箇所に集約されました。
- 堅牢性:
panic発生時やtls.Handshake()エラー時でも、接続がハイジャックされているかどうかを適切に判断し、サーバーが不必要に接続をクローズするのを防ぎます。 - 正確性:
c.hijacked()のチェックにより、ハイジャックされた接続のライフサイクル管理がアプリケーションに委ねられるというHTTPサーバーの設計原則がより厳密に守られます。
コアとなるコードの変更箇所
--- a/src/pkg/net/http/server.go
+++ b/src/pkg/net/http/server.go
@@ -702,25 +702,19 @@ func (c *conn) closeWriteAndWait() {
// Serve a new connection.
func (c *conn) serve() {
defer func() {
- err := recover()
- if err == nil {
- return
+ if err := recover(); err != nil {
+ const size = 4096
+ buf := make([]byte, size)
+ buf = buf[:runtime.Stack(buf, false)]
+ log.Printf("http: panic serving %v: %v\n%s", c.remoteAddr, err, buf)
}
-
- const size = 4096
- buf := make([]byte, size)
- buf = buf[:runtime.Stack(buf, false)]
- log.Printf("http: panic serving %v: %v\n%s", c.remoteAddr, err, buf)
-
- if c.rwc != nil { // may be nil if connection hijacked
- c.rwc.Close()
+ if !c.hijacked() {
+ c.close()
}
}()
- defer c.close()
if tlsConn, ok := c.rwc.(*tls.Conn); ok {
if err := tlsConn.Handshake(); err != nil {
-\t\t\tc.close()\n \t\t\treturn
\t\t}\n \t\tc.tlsState = new(tls.ConnectionState)\n
コアとなるコードの解説
上記の diff は、src/pkg/net/http/server.go ファイル内の (*conn).serve() メソッドに対する変更を示しています。
deferブロックの変更:- 削除された部分:
この部分では、まずerr := recover() if err == nil { return } // ... (panic ログ出力) ... if c.rwc != nil { // may be nil if connection hijacked c.rwc.Close() }recover()を呼び出し、panicが発生していなければ早期リターンしていました。panicが発生した場合はスタックトレースをログに出力し、c.rwcがnilでない場合にc.rwc.Close()を呼び出して接続をクローズしていました。このc.rwc != nilのチェックは、接続がハイジャックされている場合でもc.rwcがnilでない可能性があるため、不正確でした。 - 追加された部分:
新しいコードでは、if err := recover(); err != nil { const size = 4096 buf := make([]byte, size) buf = buf[:runtime.Stack(buf, false)] log.Printf("http: panic serving %v: %v\n%s", c.remoteAddr, err, buf) } if !c.hijacked() { c.close() }recover()の結果を直接ifステートメントで評価し、panicが発生した場合のみログ出力を行います。そして、panicの有無にかかわらず、if !c.hijacked()という条件でc.close()を呼び出すようになりました。これにより、接続がハイジャックされていない場合にのみc.close()が実行されることが保証され、より正確な接続管理が可能になります。c.close()メソッドは、内部でc.rwc.Close()を呼び出す責任を持ちます。
- 削除された部分:
defer c.close()の削除:defer func() { ... }()の直後にあったdefer c.close()の行が完全に削除されました。これにより、接続クローズのロジックがpanicハンドリングを含む単一のdeferブロックに集約され、コードの重複が解消されました。
tls.Handshake()エラーハンドリングの変更:- 削除された部分:
TLSハンドシェイクが失敗した場合に、明示的にif err := tlsConn.Handshake(); err != nil { c.close() // ここが削除される return }c.close()を呼び出していました。 - 変更後:
if err := tlsConn.Handshake(); err != nil { return }c.close()の呼び出しが削除されました。これは、ハンドシェイクエラーが発生した場合でも、関数がリターンする際に、冒頭のdeferブロックが最終的にc.close()を呼び出すため、ここでの明示的なクローズは不要になったためです。これにより、コードがさらに簡潔になりました。
- 削除された部分:
これらの変更は、serve() メソッドの堅牢性と保守性を向上させ、特に panic 発生時や接続ハイジャック時の動作をより正確に定義しています。
関連リンク
- Go CL 6970049: https://golang.org/cl/6970049
- Go GitHub リポジトリ: https://github.com/golang/go
参考にした情報源リンク
- Go言語の
deferステートメントに関する公式ドキュメントやブログ記事 - Go言語の
panicとrecoverに関する公式ドキュメントやブログ記事 - Go言語
net/httpパッケージのドキュメント - Go言語
crypto/tlsパッケージのドキュメント - Go言語のコミット履歴と関連するコードレビューコメント (CL 6970049)
- Dave Cheney氏のブログやGoに関する記事 (Goコミュニティにおける著名な貢献者の一人であるため)
- Go言語のソースコード (
src/pkg/net/http/server.go) - Go言語の
runtime.Stack関数に関するドキュメント - Go言語の
log.Printf関数に関するドキュメント - Go言語の
net/httpパッケージにおける接続ハイジャックの概念に関する情報