Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 17337] ファイルの概要

このコミットは、Go言語の標準ライブラリである net/http パッケージにおける ResponseWriter の機能拡張と最適化に関するものです。具体的には、http.ResponseWriter インターフェースを実装する response 型に WriteString メソッドを追加し、文字列データを直接書き込む際のメモリ割り当てを削減することを目的としています。

コミット

commit 33d531dfa49b7477dd51b54b870508b8b7eafee2
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date:   Mon Aug 19 22:56:54 2013 -0700

    net/http: support WriteString on the ResponseWriter
    
    Fixes #5377
    
    R=golang-dev, r
    CC=golang-dev
    https://golang.org/cl/12991046

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/33d531dfa49b7477dd51b54b870508b8b7eafee2

元コミット内容

net/http: support WriteString on the ResponseWriter

このコミットは、net/http パッケージの ResponseWriterWriteString メソッドをサポートするように変更します。これにより、文字列をHTTPレスポンスボディに書き込む際のパフォーマンスが向上します。

変更の背景

Goの io.Writer インターフェースは Write([]byte) メソッドのみを定義しています。しかし、多くの io.Writer の実装(特に bufio.Writer のようなバッファリングを行うライター)は、内部的に WriteString(string) メソッドも持っており、文字列を直接書き込む方が []byte に変換してから書き込むよりも効率的である場合があります。

従来の http.ResponseWriter は、文字列を書き込む際に []byte(string) のようにバイトスライスに変換してから Write メソッドを呼び出していました。この変換は、特に頻繁に小さな文字列を書き込む場合に、不要なメモリ割り当て(ヒープアロケーション)を引き起こし、ガベージコレクションの負荷を増大させる可能性がありました。

この問題は、GoのIssue #5377 で報告されており、WriteString メソッドの追加によってこの非効率性を解消することが提案されていました。このコミットは、その提案に対応し、net/http パッケージのパフォーマンスを向上させることを目的としています。

前提知識の解説

  • http.ResponseWriter: Goの net/http パッケージにおけるHTTPレスポンスを書き込むためのインターフェースです。通常、HTTPハンドラ関数はこれを通じてクライアントにデータを送信します。このインターフェースは io.Writer を埋め込んでいるため、Write([]byte) メソッドを持っています。
  • io.Writer: データを書き込むための基本的なインターフェースで、Write([]byte) (n int, err error) メソッドを定義しています。
  • io.StringWriter: Go 1.15で導入されたインターフェースで、WriteString(s string) (n int, err error) メソッドを定義しています。このコミットが作成された2013年時点ではまだ存在していませんでしたが、このコミットの変更は、後の io.StringWriter の概念に先行するものです。
  • メモリ割り当て (Allocation): プログラムが実行時にメモリを確保することです。Goでは、ヒープにメモリを割り当てるとガベージコレクタがそのメモリを管理する必要があり、頻繁な割り当てはパフォーマンスに影響を与える可能性があります。
  • []byte(string) 変換: Goでは、文字列をバイトスライスに変換する際に、新しいバイトスライスが作成され、文字列の内容がそこにコピーされます。これはメモリ割り当てを伴います。
  • bufio.Writer: バッファリングされたI/Oを提供するライターです。通常、io.Writer をラップして、小さな書き込みをまとめて効率化します。bufio.Writer は内部的に WriteString メソッドを持っており、これを利用することで文字列のコピーを避けることができます。

技術的詳細

このコミットの主要な技術的変更点は以下の通りです。

  1. response.WriteString メソッドの追加: src/pkg/net/http/server.go 内の response 型に WriteString(data string) (n int, err error) メソッドが追加されました。このメソッドは、文字列データを直接受け取り、内部の write ヘルパー関数を呼び出します。

  2. response.write ヘルパー関数の導入とリファクタリング: 既存の response.Write(data []byte) メソッドと新しく追加された response.WriteString(data string) メソッドの両方から呼び出される共通のヘルパー関数 write(lenData int, dataB []byte, dataS string) (n int, err error) が導入されました。 この write 関数は、dataB (バイトスライス) または dataS (文字列) のいずれか一方のみが非ゼロであることを想定しています。 write 関数内で、実際の書き込み処理は、dataB が存在する場合は w.w.Write(dataB) を呼び出し、dataS が存在する場合は w.w.WriteString(dataS) を呼び出すように分岐しています。ここで w.w は、response 型がラップしている実際の io.Writer (通常は bufio.Writer など) を指します。

  3. メモリ割り当ての最適化: response.WriteString が呼び出された場合、文字列データはバイトスライスに変換されることなく、直接下層の io.WriterWriteString メソッドに渡されます。これにより、不要なバイトスライスの割り当てとコピーが回避され、ガベージコレクションの負担が軽減されます。

  4. テストケースの追加: src/pkg/net/http/serve_test.goTestResponseWriterWriteStringAllocs という新しいテスト関数が追加されました。このテストは、WriteString を使用した場合のメモリ割り当てが、Write([]byte("...")) を使用した場合よりも少ないことを testing.AllocsPerRun を用いて検証します。これは、この変更が意図したパフォーマンス向上をもたらすことを確認するための重要なテストです。

コアとなるコードの変更箇所

src/pkg/net/http/server.go

--- a/src/pkg/net/http/server.go
+++ b/src/pkg/net/http/server.go
@@ -953,6 +953,15 @@ func (w *response) bodyAllowed() bool {
 // bufferBeforeChunkingSize smaller and having bufio's fast-paths deal
 // with this instead.
 func (w *response) Write(data []byte) (n int, err error) {
+	return w.write(len(data), data, "")
+}
+
+func (w *response) WriteString(data string) (n int, err error) {
+	return w.write(len(data), nil, data)
+}
+
+// either dataB or dataS is non-zero.
+func (w *response) write(lenData int, dataB []byte, dataS string) (n int, err error) {
 	if w.conn.hijacked() {
 		log.Print("http: response.Write on hijacked connection")
 		return 0, ErrHijacked
@@ -960,18 +969,22 @@ func (w *response) Write(data []byte) (n int, err error) {
 	if !w.wroteHeader {
 		w.WriteHeader(StatusOK)
 	}
-	if len(data) == 0 {
+	if lenData == 0 {
 		return 0, nil
 	}
 	if !w.bodyAllowed() {
 		return 0, ErrBodyNotAllowed
 	}
 
-	w.written += int64(len(data)) // ignoring errors, for errorKludge
+	w.written += int64(lenData) // ignoring errors, for errorKludge
 	if w.contentLength != -1 && w.written > w.contentLength {
 		return 0, ErrContentLength
 	}
-	return w.w.Write(data)
+	if dataB != nil {
+		return w.w.Write(dataB)
+	} else {
+		return w.w.WriteString(dataS)
+	}
 }
 
 func (w *response) finishRequest() {

src/pkg/net/http/serve_test.go

--- a/src/pkg/net/http/serve_test.go
+++ b/src/pkg/net/http/serve_test.go
@@ -1994,6 +1994,21 @@ func TestNoContentTypeOnNotModified(t *testing.T) {
 	}
 }
 
+func TestResponseWriterWriteStringAllocs(t *testing.T) {
+	ht := newHandlerTest(HandlerFunc(func(w ResponseWriter, r *Request) {
+		if r.URL.Path == "/s" {
+			io.WriteString(w, "Hello world")
+		} else {
+			w.Write([]byte("Hello world"))
+		}
+	}))
+	before := testing.AllocsPerRun(25, func() { ht.rawResponse("GET / HTTP/1.0") })
+	after := testing.AllocsPerRun(25, func() { ht.rawResponse("GET /s HTTP/1.0") })
+	if int(after) >= int(before) {
+		t.Errorf("WriteString allocs of %v >= Write allocs of %v", after, before)
+	}
+}
+
 func BenchmarkClientServer(b *testing.B) {
 	b.ReportAllocs()
 	b.StopTimer()

コアとなるコードの解説

src/pkg/net/http/server.go の変更は、response 型に WriteString メソッドを追加し、既存の Write メソッドと共通の write ヘルパー関数に処理を委譲することで、コードの重複を避けつつ最適化を実現しています。

  • func (w *response) Write(data []byte) (n int, err error): このメソッドは、これまでバイトスライスを受け取って直接書き込みを行っていましたが、変更後は w.write(len(data), data, "") を呼び出すように変更されました。これにより、実際の書き込みロジックが write ヘルパー関数に集約されます。

  • func (w *response) WriteString(data string) (n int, err error): 新しく追加されたメソッドです。文字列を受け取り、w.write(len(data), nil, data) を呼び出します。ここで nildataB に渡されることで、write 関数内で文字列パスが選択されるようになります。

  • func (w *response) write(lenData int, dataB []byte, dataS string) (n int, err error): このヘルパー関数が、実際の書き込み処理の大部分を担います。

    • lenData: 書き込むデータの長さ。dataB または dataS のどちらが使われるかにかかわらず、データの長さを一元的に扱います。
    • dataB: バイトスライスデータ。Write メソッドから呼び出された場合に設定されます。
    • dataS: 文字列データ。WriteString メソッドから呼び出された場合に設定されます。
    • if dataB != nil { return w.w.Write(dataB) } else { return w.w.WriteString(dataS) }: この条件分岐が最適化の核心です。dataBnil でない(つまり Write([]byte) が呼び出された)場合は、下層の io.WriterWrite メソッドを呼び出します。そうでない場合(つまり WriteString(string) が呼び出された)は、下層の io.WriterWriteString メソッドを呼び出します。これにより、下層のライターが WriteString をサポートしていれば、文字列からバイトスライスへの不要な変換を避けることができます。

src/pkg/net/http/serve_test.goTestResponseWriterWriteStringAllocs テストは、この最適化が実際に機能していることを確認します。

  • testing.AllocsPerRun を使用して、特定の操作が実行される際のメモリ割り当ての回数を測定します。
  • / パスへのリクエストでは w.Write([]byte("Hello world")) を使用し、/s パスへのリクエストでは io.WriteString(w, "Hello world") を使用します。
  • テストの目的は、io.WriteString を使用した場合(つまり response.WriteString が呼び出されるパス)の割り当て回数 (after) が、w.Write([]byte(...)) を使用した場合 (before) よりも少ないことを検証することです。これにより、WriteString の導入がメモリ効率の向上に寄与していることが証明されます。

関連リンク

  • Go Issue #5377: net/http: ResponseWriter should implement io.StringWriter - このコミットの背景となったIssueです。 https://github.com/golang/go/issues/5377
  • Go CL 12991046: net/http: support WriteString on the ResponseWriter - このコミットに対応するGoのコードレビューシステム (Gerrit) のチェンジリストです。 https://golang.org/cl/12991046

参考にした情報源リンク