[インデックス 11750] ファイルの概要
このコミットは、Go言語の標準ライブラリである net/http
パッケージに ServeContent
関数を追加するものです。この関数は、HTTPレスポンスとしてファイルやその他の io.ReadSeeker
インターフェースを実装するコンテンツを効率的かつ適切に提供するための汎用的なメカニズムを提供します。特に、HTTPの Range
リクエストや If-Modified-Since
ヘッダーの処理、MIMEタイプの自動検出といった重要な機能が統合されています。
コミット
commit 4539d1f307d0f8f110367bc61d11e0888feb071d
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date: Fri Feb 10 10:02:06 2012 +1100
net/http: add ServeContent
Fixes #2039
R=r, rsc, n13m3y3r, r, rogpeppe
CC=golang-dev
https://golang.org/cl/5643067
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/4539d1f307d0f8f110367bc61d11e0888feb071d
元コミット内容
net/http: add ServeContent
このコミットは、net/http
パッケージに ServeContent
関数を追加します。
Issue #2039 を修正します。
変更の背景
この変更の背景には、HTTPサーバーが静的ファイルや動的に生成されるコンテンツを効率的かつ標準的な方法で提供する必要性がありました。特に、以下の課題に対応するために ServeContent
が導入されました。
- HTTP Range リクエストの適切な処理: 大容量のファイル(動画や音声など)をストリーミングする際、クライアントはファイルの特定の部分のみを要求することがあります(例:
Range: bytes=0-499
)。これまでの実装では、このようなリクエストを適切に処理するための汎用的なメカニズムが不足しており、開発者が個別に実装する必要がありました。ServeContent
はContent-Range
ヘッダーの生成と部分的なコンテンツの送信を自動的に行います。 - If-Modified-Since ヘッダーによるキャッシュ制御: クライアントが以前に取得したコンテンツのキャッシュを持っている場合、
If-Modified-Since
ヘッダーを送信して、サーバーにコンテンツが更新されているかどうかを問い合わせます。サーバーがコンテンツが変更されていないと判断した場合、304 Not Modified
ステータスコードを返すことで、帯域幅の節約とパフォーマンスの向上が図れます。ServeContent
はこのロジックを組み込みます。 - MIMEタイプの自動検出と設定: 提供するコンテンツのMIMEタイプ(例:
text/html
,image/jpeg
)を正確に設定することは、ブラウザがコンテンツを正しく解釈するために不可欠です。ServeContent
はファイル名拡張子からのMIMEタイプ推測と、コンテンツの最初のブロックを読み取ってMIMEタイプを検出するフォールバックメカニズムを提供します。 - コードの重複排除と汎用化:
serveFile
のような既存の関数が持っていたコンテンツ提供ロジックの一部をServeContent
に集約することで、コードの重複を減らし、より汎用的なコンテンツ提供APIを提供します。これにより、開発者はファイルだけでなく、メモリ上のデータやデータベースから取得したデータなど、io.ReadSeeker
インターフェースを満たすあらゆるコンテンツを容易に提供できるようになります。
これらの機能は、堅牢で効率的なHTTPサーバーを構築する上で不可欠であり、ServeContent
の導入によって net/http
パッケージの機能が大幅に強化されました。
前提知識の解説
このコミットを理解するためには、以下の技術的な概念を理解しておく必要があります。
- HTTPプロトコル:
- HTTPヘッダー: クライアントとサーバー間で送受信されるメタデータ。特に
Content-Type
,Content-Length
,Content-Range
,Accept-Ranges
,Last-Modified
,If-Modified-Since
,Range
ヘッダーが重要です。 - HTTPステータスコード: リクエストの結果を示す3桁の数値コード。
200 OK
,206 Partial Content
,304 Not Modified
,416 Requested Range Not Satisfiable
,500 Internal Server Error
などが関連します。 - GET/HEADメソッド:
GET
はリソースの取得、HEAD
はリソースのヘッダーのみの取得に使用されます。ServeContent
はHEAD
リクエストの場合にボディを送信しないように処理します。
- HTTPヘッダー: クライアントとサーバー間で送受信されるメタデータ。特に
- MIMEタイプ (Multipurpose Internet Mail Extensions):
- インターネット上で送受信されるデータの種類を示す標準的な識別子(例:
text/html
,application/json
,image/png
)。ブラウザはMIMEタイプに基づいてコンテンツの表示方法を決定します。 mime.TypeByExtension
: ファイルの拡張子からMIMEタイプを推測するGoの関数。DetectContentType
: コンテンツの最初の数バイトを調べてMIMEタイプを検出するGoの関数。
- インターネット上で送受信されるデータの種類を示す標準的な識別子(例:
- Go言語のI/Oインターフェース:
io.Reader
: データを読み取るためのインターフェース。io.Seeker
: データの読み取り位置を移動するためのインターフェース。io.ReadSeeker
:io.Reader
とio.Seeker
の両方を組み合わせたインターフェース。ファイルのように、読み取りとシーク(位置移動)が可能なデータソースを表します。*os.File
はこのインターフェースを実装しています。io.Copy
/io.CopyN
:io.Reader
からio.Writer
へデータをコピーするためのGoのユーティリティ関数。io.CopyN
は指定されたバイト数だけコピーします。
- ファイルシステム操作:
os.SEEK_END
,os.SEEK_SET
:Seek
メソッドで使用される定数で、それぞれファイルの末尾、ファイルの先頭からの相対位置を示します。ServeContent
はコンテンツのサイズを決定するためにSeek(0, os.SEEK_END)
を使用します。
- 時間と日付のフォーマット:
time.Time
: Go言語における時刻を表す型。time.Format(TimeFormat)
: 特定のフォーマットで時刻を文字列に変換するメソッド。HTTPのLast-Modified
やIf-Modified-Since
ヘッダーで使用される日付フォーマット(RFC1123)に準拠する必要があります。
これらの概念を理解することで、ServeContent
がどのようにHTTPの仕様に準拠し、効率的なコンテンツ提供を実現しているかを深く把握できます。
技術的詳細
ServeContent
関数は、HTTPレスポンスライター (http.ResponseWriter
)、HTTPリクエスト (*http.Request
)、コンテンツ名 (name string
)、最終更新時刻 (modtime time.Time
)、およびコンテンツ自体 (content io.ReadSeeker
) を引数として受け取ります。
その内部動作は以下のステップで構成されます。
-
コンテンツサイズの取得とシーク位置のリセット:
- まず、
content.Seek(0, os.SEEK_END)
を呼び出してコンテンツの末尾にシークし、その戻り値からコンテンツの合計サイズを取得します。これにより、Content-Length
ヘッダーやContent-Range
ヘッダーを設定するために必要な情報が得られます。 - 次に、
content.Seek(0, os.SEEK_SET)
を呼び出してコンテンツの読み取り位置を先頭に戻します。これは、後続の読み取り操作がコンテンツの最初から開始されるようにするためです。 - シーク操作でエラーが発生した場合、
500 Internal Server Error
を返します。
- まず、
-
If-Modified-Since ヘッダーの処理とキャッシュ制御:
checkLastModified
ヘルパー関数が呼び出されます。- リクエストに
If-Modified-Since
ヘッダーが含まれており、かつmodtime
がそのヘッダーで指定された時刻よりも新しくない場合(つまり、コンテンツが変更されていない場合)、304 Not Modified
ステータスコードを返して処理を終了します。これにより、クライアントはキャッシュされたコンテンツを使用できます。 - コンテンツが変更されている場合、または
If-Modified-Since
ヘッダーがない場合は、Last-Modified
ヘッダーにmodtime
を設定してレスポンスに含めます。
-
Content-Type ヘッダーの設定:
- レスポンスの
Content-Type
ヘッダーがまだ設定されていない場合、以下のロジックでMIMEタイプを決定します。- まず、
name
引数(通常はファイル名)の拡張子に基づいてmime.TypeByExtension
を使用してMIMEタイプを推測します。 - 推測できなかった場合、コンテンツの最初の1024バイトを読み取り、
http.DetectContentType
を使用してMIMEタイプを検出します。この際、コンテンツの読み取り位置は再度先頭に戻されます。
- まず、
- 決定されたMIMEタイプがレスポンスの
Content-Type
ヘッダーに設定されます。
- レスポンスの
-
Range リクエストの処理:
- リクエストに
Range
ヘッダーが含まれている場合、parseRange
関数(このコミットの差分には含まれていませんが、既存のヘルパー関数)を使用して、要求されたバイト範囲を解析します。 - 現時点では、単一のバイト範囲のみがサポートされており、複数の範囲が要求された場合はエラー (
416 Requested Range Not Satisfiable
) を返します。 - 単一の範囲が有効な場合、
content.Seek
を使用してその範囲の開始位置にシークし、Content-Range
ヘッダーと206 Partial Content
ステータスコードを設定します。送信するデータのサイズも、要求された範囲の長さに調整されます。 Accept-Ranges: bytes
ヘッダーが常に設定され、サーバーがバイト範囲リクエストをサポートしていることを示します。Content-Length
ヘッダーは、送信されるデータの実際のサイズ(全体または部分)に設定されます。
- リクエストに
-
レスポンスの書き込み:
- 決定されたステータスコード (
200 OK
または206 Partial Content
) でw.WriteHeader
が呼び出されます。 - リクエストメソッドが
HEAD
でない場合(つまりGET
の場合)、io.CopyN
またはio.Copy
を使用して、content
からResponseWriter
へデータがコピーされます。Range
リクエストが処理された場合は、io.CopyN
で指定されたバイト数のみがコピーされます。
- 決定されたステータスコード (
この一連の処理により、ServeContent
はHTTPの仕様に厳密に準拠し、キャッシュ、部分コンテンツの取得、MIMEタイプ検出といった複雑な要件を自動的に処理する、堅牢なコンテンツ提供メカニズムを提供します。
コアとなるコードの変更箇所
このコミットにおける主要なコードの変更は、src/pkg/net/http/fs.go
ファイルに集中しています。
-
isText
関数の削除:fs.go
からisText
関数が削除されました。この関数は、バイトスライスがUTF-8テキストであるかどうかをヒューリスティックに判断するためのものでしたが、ServeContent
の導入により、より汎用的なhttp.DetectContentType
が使用されるようになったため、不要になりました。-func isText(b []byte) bool { - for len(b) > 0 && utf8.FullRune(b) { - rune, size := utf8.DecodeRune(b) - if size == 1 && rune == utf8.RuneError { - // decoding error - return false - } - if 0x7F <= rune && rune <= 0x9F { - return false - } - if rune < ' ' { - switch rune { - case '\n', '\r', '\t': - // okay - default: - // binary garbage - return false - } - } - b = b[size:] - } - return true -}
-
ServeContent
関数の追加: HTTPレスポンスとしてio.ReadSeeker
を実装するコンテンツを提供する新しい公開関数ServeContent
が追加されました。// ServeContent replies to the request using the content in the // provided ReadSeeker. The main benefit of ServeContent over io.Copy // is that it handles Range requests properly, sets the MIME type, and // handles If-Modified-Since requests. // // If the response's Content-Type header is not set, ServeContent // first tries to deduce the type from name's file extension and, // if that fails, falls back to reading the first block of the content // and passing it to DetectContentType. // The name is otherwise unused; in particular it can be empty and is // never sent in the response. // // If modtime is not the zero time, ServeContent includes it in a // Last-Modified header in the response. If the request includes an // If-Modified-Since header, ServeContent uses modtime to decide // whether the content needs to be sent at all. // // The content's Seek method must work: ServeContent uses // a seek to the end of the content to determine its size. // // 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 } serveContent(w, req, name, modtime, size, content) }
-
serveContent
ヘルパー関数の追加:ServeContent
から呼び出される内部ヘルパー関数serveContent
が追加されました。この関数が実際のコンテンツ提供ロジックの大部分を担います。// 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) { // ... (詳細なロジックは「技術的詳細」セクションを参照) ... }
-
checkLastModified
ヘルパー関数の追加:If-Modified-Since
ヘッダーとLast-Modified
ヘッダーの処理をカプセル化するためのヘルパー関数checkLastModified
が追加されました。// modtime is the modification time of the resource to be served, or IsZero(). // return value is whether this request is now complete. func checkLastModified(w ResponseWriter, r *Request, modtime time.Time) bool { if modtime.IsZero() { return false } if t, err := time.Parse(TimeFormat, r.Header.Get("If-Modified-Since")); err == nil && modtime.After(t) { w.WriteHeader(StatusNotModified) return true } w.Header().Set("Last-Modified", modtime.UTC().Format(TimeFormat)) return false }
-
serveFile
関数のリファクタリング: 既存のserveFile
関数が、新しく追加されたserveContent
とcheckLastModified
を利用するように変更されました。これにより、serveFile
内の重複するロジックが削除され、コードが簡潔になりました。--- a/src/pkg/net/http/fs.go +++ b/src/pkg/net/http/fs.go @@ -148,14 +238,11 @@ func serveFile(w ResponseWriter, r *Request, fs FileSystem, name string, redirec } } - if t, err := time.Parse(TimeFormat, r.Header.Get("If-Modified-Since")); err == nil && !d.ModTime().After(t) { - w.WriteHeader(StatusNotModified) - return - } - w.Header().Set("Last-Modified", d.ModTime().UTC().Format(TimeFormat)) - // use contents of index.html for directory, if present if d.IsDir() { + if checkLastModified(w, r, d.ModTime()) { + return + } index := name + indexPage ff, err := fs.Open(index) if err == nil { @@ -174,60 +261,7 @@ func serveFile(w ResponseWriter, r *Request, fs FileSystem, name string, redirec return } - // serve file - size := d.Size() - code := StatusOK - - // If Content-Type isn't set, use the file's extension to find it. - if w.Header().Get("Content-Type") == "" { - ctype := mime.TypeByExtension(filepath.Ext(name)) - if ctype == "" { - // read a chunk to decide between utf-8 text and binary - var buf [1024]byte - n, _ := io.ReadFull(f, buf[:]) - b := buf[:n] - if isText(b) { - ctype = "text/plain; charset=utf-8" - } else { - // generic binary - ctype = "application/octet-stream" - } - f.Seek(0, os.SEEK_SET) // rewind to output whole file - } - w.Header().Set("Content-Type", ctype) - } - - // handle Content-Range header. - // TODO(adg): handle multiple ranges - ranges, err := parseRange(r.Header.Get("Range"), size) - if err == nil && len(ranges) > 1 { - err = errors.New("multiple ranges not supported") - } - if err != nil { - Error(w, err.Error(), StatusRequestedRangeNotSatisfiable) - return - } - if len(ranges) == 1 { - ra := ranges[0] - if _, err := f.Seek(ra.start, os.SEEK_SET); err != nil { - Error(w, err.Error(), StatusRequestedRangeNotSatisfiable) - return - } - size = ra.length - code = StatusPartialContent - w.Header().Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", ra.start, ra.start+ra.length-1, d.Size())) - } - - w.Header().Set("Accept-Ranges", "bytes") - if w.Header().Get("Content-Encoding") == "" { - w.Header().Set("Content-Length", strconv.FormatInt(size, 10)) - } - - w.WriteHeader(code) - - if r.Method != "HEAD" { - io.CopyN(w, f, size) - } + serveContent(w, r, d.Name(), d.ModTime(), d.Size(), f) }
-
テストファイルの追加と修正:
src/pkg/net/http/fs_test.go
にTestServeContent
が追加され、新しいServeContent
関数の動作が検証されています。また、既存のテストヘルパー関数getBody
も、テスト名引数を追加するように修正されています。--- a/src/pkg/net/http/fs_test.go +++ b/src/pkg/net/http/fs_test.go @@ -306,17 +307,66 @@ func TestServeIndexHtml(t *testing.T) { } } +func TestServeContent(t *testing.T) { + type req struct { + name string + modtime time.Time + content io.ReadSeeker + } + ch := make(chan req, 1) + ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) { + p := <-ch + ServeContent(w, r, p.name, p.modtime, p.content) + })) + defer ts.Close() + + css, err := os.Open("testdata/style.css") + if err != nil { + t.Fatal(err) + } + defer css.Close() + + ch <- req{"style.css", time.Time{}, css} + res, err := Get(ts.URL) + if err != nil { + t.Fatal(err) + } + if g, e := res.Header.Get("Content-Type"), "text/css; charset=utf-8"; g != e { + t.Errorf("style.css: content type = %q, want %q", g, e) + } + if g := res.Header.Get("Last-Modified"); g != "" { + t.Errorf("want empty Last-Modified; got %q", g) + } + + fi, err := css.Stat() + if err != nil { + t.Fatal(err) + } + ch <- req{"style.html", fi.ModTime(), css} + res, err = Get(ts.URL) + if err != nil { + t.Fatal(err) + } + if g, e := res.Header.Get("Content-Type"), "text/html; charset=utf-8"; g != e { + t.Errorf("style.html: content type = %q, want %q", g, e) + } + if g := res.Header.Get("Last-Modified"); g == "" { + t.Errorf("want non-empty last-modified") + } +} + -func getBody(t *testing.T, req Request) (*Response, []byte) { +func getBody(t *testing.T, testName string, req Request) (*Response, []byte) { r, err := DefaultClient.Do(&req) if err != nil { - t.Fatal(req.URL.String(), "send:", err) + t.Fatalf("%s: for URL %q, send error: %v", testName, req.URL.String(), err) } b, err := ioutil.ReadAll(r.Body) if err != nil { - t.Fatal("reading Body:", err) + t.Fatalf("%s: for URL %q, reading body: %v", testName, req.URL.String(), err) } return r, b }
これらの変更により、net/http
パッケージはより強力で柔軟なコンテンツ提供機能を持つようになりました。
コアとなるコードの解説
このコミットのコアとなるコードは、ServeContent
関数とその内部で呼び出される serveContent
ヘルパー関数です。
ServeContent
関数
func ServeContent(w ResponseWriter, req *Request, name string, modtime time.Time, content io.ReadSeeker) {
// 1. コンテンツの合計サイズを取得するために、末尾にシーク
size, err := content.Seek(0, os.SEEK_END)
if err != nil {
Error(w, "seeker can't seek", StatusInternalServerError)
return
}
// 2. 読み取り位置を先頭に戻す
_, err = content.Seek(0, os.SEEK_SET)
if err != nil {
Error(w, "seeker can't seek", StatusInternalServerError)
return
}
// 3. 実際のコンテンツ提供ロジックを serveContent ヘルパー関数に委譲
serveContent(w, req, name, modtime, size, content)
}
- 目的:
ServeContent
は、外部から呼び出される主要なAPIです。io.ReadSeeker
インターフェースを実装する任意のコンテンツ(例:*os.File
)をHTTPレスポンスとして提供します。 io.ReadSeeker
の要件: この関数は、content
引数がSeek
メソッドを正しく実装していることを前提としています。これは、コンテンツの合計サイズを決定するため(Seek(0, os.SEEK_END)
)と、部分的なコンテンツ提供のために読み取り位置を移動するため(Seek(offset, os.SEEK_SET)
)に不可欠です。- エラーハンドリング:
Seek
操作が失敗した場合、500 Internal Server Error
をクライアントに返します。 - 委譲: 実際の複雑なロジックは、内部ヘルパー関数である
serveContent
に委譲されています。これにより、APIの公開インターフェースをシンプルに保ちつつ、内部実装の柔軟性を高めています。
serveContent
関数
func serveContent(w ResponseWriter, r *Request, name string, modtime time.Time, size int64, content io.ReadSeeker) {
// 1. If-Modified-Since ヘッダーのチェックとキャッシュ制御
if checkLastModified(w, r, modtime) {
return // コンテンツが変更されていない場合、304を返して終了
}
code := StatusOK // デフォルトのステータスコードは200 OK
// 2. Content-Type ヘッダーの設定
if w.Header().Get("Content-Type") == "" {
ctype := mime.TypeByExtension(filepath.Ext(name)) // 拡張子から推測
if ctype == "" {
// 推測できなかった場合、コンテンツの最初のブロックを読み取り DetectContentType で検出
var buf [1024]byte
n, _ := io.ReadFull(content, buf[:])
b := buf[:n]
ctype = DetectContentType(b)
_, err := content.Seek(0, os.SEEK_SET) // 読み取り位置を先頭に戻す
if err != nil {
Error(w, "seeker can't seek", StatusInternalServerError)
return
}
}
w.Header().Set("Content-Type", ctype)
}
// 3. Range リクエストの処理
sendSize := size // 送信するデータの初期サイズはコンテンツ全体
if size >= 0 { // サイズが不明でない場合のみRange処理を行う
ranges, err := parseRange(r.Header.Get("Range"), size)
if err == nil && len(ranges) > 1 {
err = errors.New("multiple ranges not supported") // 複数範囲は未サポート
}
if err != nil {
Error(w, err.Error(), StatusRequestedRangeNotSatisfiable) // Rangeヘッダーが無効な場合
return
}
if len(ranges) == 1 { // 単一のRangeリクエストの場合
ra := ranges[0]
if _, err := content.Seek(ra.start, os.SEEK_SET); err != nil { // 要求された開始位置にシーク
Error(w, err.Error(), StatusRequestedRangeNotSatisfiable)
return
}
sendSize = ra.length // 送信するサイズをRangeの長さに設定
code = StatusPartialContent // ステータスコードを206 Partial Content に変更
w.Header().Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", ra.start, ra.start+ra.length-1, size))
}
w.Header().Set("Accept-Ranges", "bytes") // バイト範囲リクエストをサポートすることを示す
if w.Header().Get("Content-Encoding") == "" {
w.Header().Set("Content-Length", strconv.FormatInt(sendSize, 10)) // 送信するデータの長さを設定
}
}
// 4. ヘッダーを書き込み、レスポンスボディをコピー
w.WriteHeader(code) // ステータスコードを書き込む
if r.Method != "HEAD" { // HEADリクエストの場合はボディを送信しない
if sendSize == -1 { // サイズが不明な場合(通常は発生しないが念のため)
io.Copy(w, content)
} else {
io.CopyN(w, content, sendSize) // 指定されたバイト数だけコピー
}
}
}
checkLastModified
の利用: キャッシュ制御ロジックをcheckLastModified
に委譲し、コードの重複を避けています。- MIMEタイプ検出の優先順位:
- 既存の
Content-Type
ヘッダーが設定されていればそれを使用。 name
の拡張子からmime.TypeByExtension
で推測。- それでも不明な場合、コンテンツの最初の1024バイトを
DetectContentType
で分析。
- 既存の
- Range リクエストの堅牢な処理:
parseRange
を使用してRange
ヘッダーを解析。- 現時点では単一の範囲のみをサポートし、複数範囲はエラーとする。
- 要求された範囲に基づいて
Content-Range
ヘッダーを設定し、206 Partial Content
ステータスコードを返す。 Accept-Ranges: bytes
を設定し、クライアントにバイト範囲リクエストのサポートを通知。Content-Length
を送信するデータの実際の長さに設定。
HEAD
メソッドのサポート:HEAD
リクエストの場合、ボディは送信せず、ヘッダーのみを送信します。これは、リソースのメタデータのみを取得したい場合に効率的です。io.CopyN
による効率的なデータ転送:Range
リクエストやContent-Length
が設定されている場合、io.CopyN
を使用して必要なバイト数だけを効率的にコピーします。これにより、不要なデータ転送を防ぎます。
これらの関数は連携して、HTTPの複雑な仕様に準拠しつつ、Go言語のシンプルで効率的なI/Oインターフェースを活用して、汎用的なコンテンツ提供機能を実現しています。
関連リンク
- Go Issue 2039: https://github.com/golang/go/issues/2039
- Gerrit Change-ID: https://golang.org/cl/5643067
- Go
net/http
パッケージドキュメント: https://pkg.go.dev/net/http - Go
io
パッケージドキュメント: https://pkg.go.dev/io - Go
mime
パッケージドキュメント: https://pkg.go.dev/mime
参考にした情報源リンク
- HTTP/1.1 RFC 2616 (Range Requests, If-Modified-Since, Last-Modified): https://www.rfc-editor.org/rfc/rfc2616
- MIME types: https://developer.mozilla.org/ja/docs/Web/HTTP/Basics_of_HTTP/MIME_types
- Go言語の
io.ReadSeeker
インターフェースに関する解説記事など (一般的なGoのI/Oに関する情報源) - Go言語の
net/http
パッケージのソースコード (コミット前後の比較) - Go言語のIssueトラッカー (Issue 2039の詳細)
- Go言語のGerritコードレビューシステム (Change-ID 5643067の詳細)