[インデックス 16721] ファイルの概要
このコミットは、Go言語の標準ライブラリ net/http
パッケージにおける ServeContent
関数の振る舞いを改善するものです。具体的には、HTTPレスポンスを生成する際に、コンテンツのサイズを決定するための Seek
操作を、本当に必要になるまで遅延させるように変更しています。これにより、特に 304 Not Modified
レスポンスを返す場合に、不要なI/O操作を削減し、効率性と堅牢性を向上させています。
コミット
commit d178c016c2b6ae2403986c730d8d11ed95a8211f
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date: Wed Jul 10 13:29:52 2013 +1000
net/http: in ServeContent, don't seek on content until necessary
R=golang-dev, adg
CC=golang-dev
https://golang.org/cl/11080043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/d178c016c2b6ae2403986c730d8d11ed95a8211f
元コミット内容
net/http: in ServeContent, don't seek on content until necessary
このコミットは、net/http
パッケージの ServeContent
関数において、コンテンツの Seek
操作を必要になるまで行わないように変更します。
変更の背景
net/http.ServeContent
関数は、HTTPリクエストに対して io.ReadSeeker
インターフェースを実装するコンテンツ(例えばファイル)を効率的に提供するために設計されています。この関数は、コンテンツのサイズを把握するために、提供する io.ReadSeeker
の Seek
メソッドを呼び出し、コンテンツの終端に移動してから先頭に戻るという操作を初期段階で行っていました。
しかし、HTTPプロトコルにはキャッシュ機構があり、クライアントが If-Modified-Since
や If-None-Match
といったヘッダーを送信することで、サーバーはコンテンツが変更されていない場合に 304 Not Modified
ステータスコードを返すことができます。この場合、サーバーは実際のコンテンツを送信する必要がなく、したがってコンテンツのサイズを事前に知る必要もありません。
変更前の実装では、304 Not Modified
レスポンスを返すことが可能な状況であっても、無条件に Seek
操作を実行していました。これは、特に Seek
操作がコストの高いI/O操作である場合(例えば、ネットワーク越しに提供されるコンテンツや、仮想的なコンテンツの場合)、無駄なリソース消費やパフォーマンスの低下を招く可能性がありました。また、Seek
操作をサポートしない io.ReadSeeker
の実装に対しては、不必要なエラーを引き起こす可能性もありました。
このコミットの目的は、このような不要な Seek
操作を排除し、ServeContent
の効率性と汎用性を向上させることにあります。
前提知識の解説
1. io.Reader
, io.Seeker
, io.ReadSeeker
インターフェース
Go言語の io
パッケージは、I/O操作のための基本的なインターフェースを提供します。
io.Reader
: データを読み込むための最も基本的なインターフェースです。Read(p []byte) (n int, err error)
メソッドを持ち、バイトスライスp
にデータを読み込み、読み込んだバイト数n
とエラーを返します。io.Seeker
: データの読み書き位置(オフセット)を変更するためのインターフェースです。Seek(offset int64, whence int) (int64, error)
メソッドを持ちます。offset
: シークするバイト数。whence
: シークの基準位置を指定します。os.SEEK_SET
(ファイルの先頭から)、os.SEEK_CUR
(現在の位置から)、os.SEEK_END
(ファイルの終端から)のいずれかです。- 戻り値は、新しいオフセットとエラーです。
io.ReadSeeker
:io.Reader
とio.Seeker
の両方の機能を兼ね備えたインターフェースです。ファイルのように、読み込みとシークの両方が可能なデータソースを表します。
2. HTTPキャッシュと 304 Not Modified
HTTPプロトコルは、クライアントとサーバー間の通信を効率化するためにキャッシュ機構を提供します。
Last-Modified
ヘッダー: サーバーがリソースの最終更新日時をクライアントに伝えます。ETag
ヘッダー: サーバーがリソースの特定のバージョンを識別するための不透明なエンティティタグ(通常はハッシュ値など)をクライアントに伝えます。If-Modified-Since
リクエストヘッダー: クライアントが、指定された日時以降にリソースが変更された場合にのみリソースを要求します。If-None-Match
リクエストヘッダー: クライアントが、指定されたETag
と一致しない場合にのみリソースを要求します。
サーバーはこれらのリクエストヘッダーを受け取ると、リソースが変更されていないことを確認した場合、304 Not Modified
ステータスコードを返します。この場合、レスポンスボディは空であり、クライアントは自身のキャッシュされたコピーを使用します。これにより、ネットワーク帯域幅の節約とレスポンス時間の短縮が実現されます。
3. net/http.ServeContent
関数
net/http.ServeContent
は、io.ReadSeeker
インターフェースを実装するコンテンツをHTTPレスポンスとして提供するためのユーティリティ関数です。この関数は、コンテンツのMIMEタイプ検出、Last-Modified
や ETag
ヘッダーの処理、レンジリクエスト(Content-Range
ヘッダー)の処理など、HTTPコンテンツ配信に必要な多くの機能を提供します。
技術的詳細
変更の核心は、ServeContent
関数がコンテンツのサイズを取得する方法にあります。
変更前:
ServeContent
関数は、受け取った io.ReadSeeker
(content
) に対して、まず content.Seek(0, os.SEEK_END)
を呼び出してコンテンツの終端に移動し、その戻り値からコンテンツの全長 (size
) を取得していました。その後、content.Seek(0, os.SEEK_SET)
を呼び出してコンテンツの先頭に戻していました。これらの操作は、ServeContent
の処理の非常に早い段階で、checkLastModified
などのキャッシュ関連のチェックが行われる前に行われていました。
// 変更前のServeContentの抜粋
func ServeContent(w ResponseWriter, req *Request, name string, modtime time.Time, content io.ReadSeeker) {
size, err := content.Seek(0, os.SEEK_END) // ここでSeekが実行される
if err != nil { /* ... */ }
_, err = content.Seek(0, os.SEEK_SET) // ここでもSeekが実行される
if err != nil { /* ... */ }
serveContent(w, req, name, modtime, size, content)
}
変更後:
このコミットでは、コンテンツのサイズを取得するロジックを sizeFunc func() (int64, error)
という関数型に抽象化し、これを ServeContent
および内部で呼び出される serveContent
関数に渡すようにしました。
ServeContent
関数は、sizeFunc
を定義する際に、以前の Seek
操作をこの関数内にカプセル化します。
// 変更後のServeContentの抜粋
func ServeContent(w ResponseWriter, req *Request, name string, modtime time.Time, content io.ReadSeeker) {
sizeFunc := func() (int64, error) {
size, err := content.Seek(0, os.SEEK_END) // Seek操作はここに含まれる
if err != nil { return 0, errSeeker }
_, err = content.Seek(0, os.SEEK_SET)
if err != nil { return 0, errSeeker }
return size, nil
}
serveContent(w, req, name, modtime, sizeFunc, content)
}
そして、serveContent
関数内で、checkLastModified
が呼び出された後に初めて sizeFunc()
が呼び出されるように変更されました。
// 変更後のserveContentの抜粋
func serveContent(w ResponseWriter, r *Request, name string, modtime time.Time, sizeFunc func() (int64, error), content io.ReadSeeker) {
if checkLastModified(w, r, modtime) {
return // 304 Not Modifiedの場合、ここで処理が終了し、sizeFuncは呼び出されない
}
// ...
size, err := sizeFunc() // 304でない場合のみ、ここでsizeFuncが呼び出され、Seekが実行される
if err != nil { /* ... */ }
// ...
}
この変更により、checkLastModified
が true
を返し、304 Not Modified
レスポンスが送信される場合、sizeFunc
は決して呼び出されず、結果として content
に対する Seek
操作も実行されません。これにより、不要なI/O操作が回避され、パフォーマンスが向上し、Seek
操作がコスト高またはサポートされていない io.ReadSeeker
の実装に対してもより堅牢になります。
また、errSeeker
という新しい内部エラー変数が導入されました。これは、sizeFunc
内で Seek
操作が失敗した場合に返されるエラーです。このエラーは、元の Seek
エラーの詳細をHTTPレスポンスに含めないようにするために使用されます。これにより、内部的なエラーメッセージが外部に漏洩するのを防ぎ、セキュリティと情報隠蔽の観点からも改善されています。
テストコード (fs_test.go
) では、panicOnSeek
というダミーの io.ReadSeeker
実装が追加されました。これは Seek
メソッドが呼び出されるとパニックを発生させるものです。この panicOnSeek
を使用したテストケース (not_modified_etag_no_seek
) を追加することで、304 Not Modified
のシナリオで実際に Seek
操作が行われないことを検証しています。
コアとなるコードの変更箇所
src/pkg/net/http/fs.go
--- a/src/pkg/net/http/fs.go
+++ b/src/pkg/net/http/fs.go
@@ -105,23 +105,31 @@ func dirList(w ResponseWriter, f File) {
//
// Note that *os.File implements the io.ReadSeeker interface.
func ServeContent(w ResponseWriter, req *Request, name string, modtime time.Time, content io.ReadSeeker) {
- size, err := content.Seek(0, os.SEEK_END)
- if err != nil {
- Error(w, "seeker can't seek", StatusInternalServerError)
- return
- }
- _, err = content.Seek(0, os.SEEK_SET)
- if err != nil {
- Error(w, "seeker can't seek", StatusInternalServerError)
- return
+ sizeFunc := func() (int64, error) {
+ size, err := content.Seek(0, os.SEEK_END)
+ if err != nil {
+ return 0, errSeeker
+ }
+ _, err = content.Seek(0, os.SEEK_SET)
+ if err != nil {
+ return 0, errSeeker
+ }
+ return size, nil
}
- serveContent(w, req, name, modtime, size, content)
+ serveContent(w, req, name, modtime, sizeFunc, content)
}
+// errSeeker is returned by ServeContent's sizeFunc when the content
+// doesn't seek properly. The underlying Seeker's error text isn't
+// included in the sizeFunc reply so it's not sent over HTTP to end
+// users.
+var errSeeker = errors.New("seeker can't seek")
+
// if name is empty, filename is unknown. (used for mime type, before sniffing)
// if modtime.IsZero(), modtime is unknown.
// content must be seeked to the beginning of the file.
-func serveContent(w ResponseWriter, r *Request, name string, modtime time.Time, size int64, content io.ReadSeeker) {
+// The sizeFunc is called at most once. Its error, if any, is sent in the HTTP response.
+func serveContent(w ResponseWriter, r *Request, name string, modtime time.Time, sizeFunc func() (int64, error), content io.ReadSeeker) {
if checkLastModified(w, r, modtime) {
return
}
@@ -151,6 +159,12 @@ func serveContent(w ResponseWriter, r *Request, name string, modtime time.Time,
w.Header().Set("Content-Type", ctype)
}
+ size, err := sizeFunc()
+ if err != nil {
+ Error(w, err.Error(), StatusInternalServerError)
+ return
+ }
+
// handle Content-Range header.
sendSize := size
var sendContent io.Reader = content
@@ -378,7 +392,8 @@ func serveFile(w ResponseWriter, r *Request, fs FileSystem, name string, redirec
}
// serverContent will check modification time
- serveContent(w, r, d.Name(), d.ModTime(), d.Size(), f)
+ sizeFunc := func() (int64, error) { return d.Size(), nil }
+ serveContent(w, r, d.Name(), d.ModTime(), sizeFunc, f)
}
// localRedirect gives a Moved Permanently response.
src/pkg/net/http/fs_test.go
--- a/src/pkg/net/http/fs_test.go
+++ b/src/pkg/net/http/fs_test.go
@@ -567,7 +567,10 @@ func TestServeContent(t *testing.T) {
defer ts.Close()\n
type testCase struct {
-\t\tfile string
+\t\t// One of file or content must be set:
+\t\tfile string
+\t\tcontent io.ReadSeeker
+\n
\t\tmodtime time.Time
\t\tserveETag string // optional
\t\tserveContentType string // optional
@@ -615,6 +618,14 @@ func TestServeContent(t *testing.T) {
\t\t\t},\n
\t\t\twantStatus: 304,\n
\t\t},\n
+\t\t\"not_modified_etag_no_seek\": {\n
+\t\t\tcontent: panicOnSeek{nil}, // should never be called\n
+\t\t\tserveETag: `\"foo\"`,\n
+\t\t\treqHeader: map[string]string{\n
+\t\t\t\t\"If-None-Match\": `\"foo\"`,\n
+\t\t\t},\n
+\t\t\twantStatus: 304,\n
+\t\t},\n
\t\t\"range_good\": {\n
\t\t\tfile: \"testdata/style.css\",\n
\t\t\tserveETag: `\"A\"`,\n
@@ -638,15 +649,21 @@ func TestServeContent(t *testing.T) {
\t\t},\n
\t}\n
\tfor testName, tt := range tests {\n
-\t\tf, err := os.Open(tt.file)\n
-\t\tif err != nil {\n
-\t\t\tt.Fatalf(\"test %q: %v\", testName, err)\n
+\t\tvar content io.ReadSeeker\n
+\t\tif tt.file != \"\" {\n
+\t\t\tf, err := os.Open(tt.file)\n
+\t\t\tif err != nil {\n
+\t\t\t\tt.Fatalf(\"test %q: %v\", testName, err)\n
+\t\t\t}\n
+\t\t\tdefer f.Close()\n
+\t\t\tcontent = f\n+\t\t} else {\n
+\t\t\tcontent = tt.content\n \t\t}\n-\t\tdefer f.Close()\n
\n \t\tservec <- serveParam{\n \t\t\tname: filepath.Base(tt.file),\n-\t\t\tcontent: f,\n+\t\t\tcontent: content,\n \t\t\tmodtime: tt.modtime,\n \t\t\tetag: tt.serveETag,\n \t\t\tcontentType: tt.serveContentType,\n@@ -763,3 +780,5 @@ func TestLinuxSendfileChild(*testing.T) {\n \t\tpanic(err)\n \t}\n }\n+\n+type panicOnSeek struct{ io.ReadSeeker }\n```
## コアとなるコードの解説
### `ServeContent` 関数の変更
* **`size` 引数の削除と `sizeFunc` の導入**:
変更前は `ServeContent` が直接 `content.Seek` を呼び出して `size` を計算し、その `size` を `serveContent` に渡していました。
変更後は、`sizeFunc := func() (int64, error) { ... }` という匿名関数を定義し、この関数内で `Seek` 操作を実行するようにしました。この `sizeFunc` が `serveContent` に渡されます。これにより、`Seek` 操作の実行タイミングを制御できるようになります。
* **エラーハンドリングの改善**:
`sizeFunc` 内で `content.Seek` がエラーを返した場合、直接そのエラーを返すのではなく、新しく定義された内部エラー `errSeeker` を返すようにしました。これは、`Seek` 操作の具体的なエラーメッセージがHTTPレスポンスとしてクライアントに漏洩するのを防ぐためです。
### `serveContent` 関数の変更
* **`sizeFunc` の呼び出しタイミングの変更**:
変更前は `serveContent` が `size` を直接引数として受け取っていました。
変更後は `sizeFunc` を引数として受け取り、`checkLastModified(w, r, modtime)` の呼び出し後、かつ `304 Not Modified` レスポンスが返されない場合にのみ `size, err := sizeFunc()` を呼び出すようにしました。これにより、キャッシュがヒットして `304` が返される場合には、`Seek` 操作が完全にスキップされます。
### `serveFile` 関数の変更
* `serveFile` 関数も `ServeContent` を呼び出しているため、その引数に合わせて `sizeFunc` を生成して渡すように変更されました。`os.File` の `Size()` メソッドは `Seek` を必要としないため、シンプルな `sizeFunc` が定義されています。
### テストコード (`fs_test.go`) の変更
* **`testCase` 構造体の拡張**:
テストケースの定義に `content io.ReadSeeker` フィールドが追加され、ファイルパスだけでなく、直接 `io.ReadSeeker` の実装をテストに渡せるようになりました。
* **`panicOnSeek` 構造体の追加**:
`io.ReadSeeker` インターフェースを実装し、`Seek` メソッドが呼び出されると `panic` を発生させる `panicOnSeek` 型が追加されました。
* **`not_modified_etag_no_seek` テストケースの追加**:
この新しいテストケースでは、`panicOnSeek` を `content` として使用し、`If-None-Match` ヘッダーを送信して `304 Not Modified` レスポンスが期待される状況をシミュレートします。このテストが成功するということは、`304` レスポンスの場合に `Seek` メソッドが実際に呼び出されていないことを証明します。もし `Seek` が呼び出されれば、`panicOnSeek` によってテストは失敗します。
これらの変更により、`net/http.ServeContent` はより効率的で堅牢になり、特にキャッシュが有効に機能するシナリオにおいて、不要なI/Oオーバーヘッドを削減できるようになりました。
## 関連リンク
* Go言語 `io` パッケージのドキュメント: [https://pkg.go.dev/io](https://pkg.go.dev/io)
* Go言語 `net/http` パッケージのドキュメント: [https://pkg.go.dev/net/http](https://pkg.go.dev/net/http)
* HTTP/1.1 RFC 2616 (特にセクション 14.25 If-Modified-Since, 14.26 If-None-Match, 10.3.5 304 Not Modified): [https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.25](https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.25)
## 参考にした情報源リンク
* Go CL 11080043: [https://golang.org/cl/11080043](https://golang.org/cl/11080043) (元の変更リスト)
* Go issue tracker (関連する可能性のあるissue): Goのコミットメッセージには直接issue番号が記載されていませんが、通常は関連するissueが存在します。この変更はパフォーマンスと堅牢性の改善であるため、そのような目的のissueが背景にある可能性があります。