[インデックス 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
パッケージにおける接続ハイジャックの概念に関する情報