[インデックス 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の詳細)