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

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

このコミットは、Go言語の標準ライブラリであるnet/httpstringsbytesパッケージにおける重要な修正を含んでいます。主な目的は、net/httpパッケージで発生していたデータ競合(data race)のバグを修正すること、そしてstrings.Readerおよびbytes.ReaderReadメソッドの挙動の一部を元に戻すことです。特に、ゼロバイトを読み取る際のメモリ書き込みの有無に関する変更が対象となっています。

コミット

commit 13ea1fd233465bc5dd410c8c64c8120ab249ab69
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date:   Fri Apr 25 06:44:51 2014 -0700

    net/http, strings, bytes: fix http race, revert part of Reader behavior change
    
    I fixed this data race regression in two ways: in net/http itself, and also
    partially reverting the change from https://golang.org/cl/77580046 .
    Previously a Read from a strings.Reader or bytes.Reader returning 0 bytes
    would not be a memory write. After 77580046 it was. This reverts that back
    in case others depended on that. Also adds tests.
    
    Fixes #7856
    
    LGTM=ruiu, iant
    R=iant, ruiu
    CC=golang-codereviews, gri
    https://golang.org/cl/94740044

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

https://github.com/golang/go/commit/13ea1fd233465bc5dd410c8c64c8120ab249ab69

元コミット内容

net/http, strings, bytes: fix http race, revert part of Reader behavior change

このコミットは、net/httpにおけるHTTPの競合状態を修正し、Readerの挙動変更の一部を元に戻します。

このデータ競合の回帰は2つの方法で修正されました。1つはnet/http自体で、もう1つはhttps://golang.org/cl/77580046からの変更を部分的に元に戻すことです。以前は、strings.Readerまたはbytes.ReaderからのReadが0バイトを返してもメモリ書き込みは発生しませんでした。しかし、77580046の変更後には発生するようになりました。このコミットは、他のコードがその挙動に依存している可能性を考慮し、元の挙動に戻します。また、テストも追加されています。

Issue #7856 を修正します。

変更の背景

このコミットの背景には、主に以下の2つの問題がありました。

  1. net/httpパッケージにおけるデータ競合(Data Race)の発生: Goのnet/httpパッケージのサーバー実装において、特定の条件下でデータ競合が発生するバグ(Issue #7856)が報告されました。この競合は、特にリクエストボディが空の場合に顕著であったと推測されます。データ競合は、複数のゴルーチンが同時に同じメモリ領域にアクセスし、少なくとも1つが書き込みを行う場合に発生し、予測不能な動作やクラッシュを引き起こす可能性があります。

  2. strings.Readerおよびbytes.ReaderReadメソッドの意図しない挙動変更: 以前のコミット(https://golang.org/cl/77580046)によって、strings.Readerbytes.ReaderReadメソッドが、たとえ読み取るバイト数が0であっても、内部状態(具体的にはprevRuneフィールド)にメモリ書き込みを行うように変更されました。この変更自体は、おそらくprevRuneの確実なリセットを意図したものだったと考えられます。しかし、この「ゼロバイト読み取り時のメモリ書き込み」という新しい挙動が、上記net/httpのデータ競合の一因となっていた、あるいは他の既存コードの前提を崩す可能性がありました。特に、複数のゴルーチンから同時にReadが呼ばれ、そのうちのいくつかがゼロバイト読み取りを行う場合に、prevRuneへの書き込みが競合を引き起こす可能性があったのです。

このコミットは、これら2つの問題を同時に解決しようとするものです。net/http側の修正と、Readerの挙動を部分的に元に戻すことで、より堅牢なシステムを目指しています。

前提知識の解説

このコミットを理解するためには、以下のGo言語の概念と関連技術についての知識が役立ちます。

  • Go言語の並行処理とゴルーチン (Goroutines): Go言語は、軽量なスレッドであるゴルーチンを用いて並行処理を容易に記述できます。ゴルーチンはOSのスレッドよりもはるかに軽量で、数千、数万のゴルーチンを同時に実行することが可能です。

  • データ競合 (Data Race): 複数のゴルーチンが同時に同じメモリ領域にアクセスし、そのうち少なくとも1つが書き込みを行う場合に発生するバグです。データ競合が発生すると、プログラムの動作が非決定論的になり、デバッグが非常に困難になります。Goにはgo run -raceというデータ競合検出ツールがあり、開発中に競合を特定するのに役立ちます。

  • io.Readerインターフェース: Go言語におけるデータの読み込み操作を抽象化する基本的なインターフェースです。 type Reader interface { Read(p []byte) (n int, err error) } Readメソッドは、pに最大len(p)バイトを読み込み、読み込んだバイト数nとエラーerrを返します。nが0でerrnilの場合、それはReadが何も読み込まなかったが、エラーでもないことを意味します(例えば、非ブロックI/Oでデータがまだ利用できない場合など)。nが0でerrio.EOFの場合、それはストリームの終端に達したことを意味します。

  • strings.Readerbytes.Reader: それぞれ文字列(string)とバイトスライス([]byte)をio.Readerインターフェースとして扱うための型です。これらは、メモリ上のデータをファイルのように読み込む際に便利です。内部的には、読み込み位置を示すインデックス(i)と、UnreadRuneメソッドのために直前のルーンのサイズを記憶するprevRuneなどのフィールドを持っています。

  • net/httpパッケージ: Go言語でHTTPクライアントおよびサーバーを構築するための標準ライブラリです。サーバーは複数のクライアントからのリクエストを並行して処理するため、内部でゴルーチンを多用します。そのため、データ競合が発生しやすい環境でもあります。

  • io.Copyio.WriterToインターフェース: io.Copy(dst io.Writer, src io.Reader)関数は、srcからdstへデータをコピーします。この関数は、srcio.WriterToインターフェースを実装している場合、そのWriteToメソッドを優先的に使用します。WriteToメソッドは、バッファを介さずに直接データを書き込むことができるため、効率的なコピーが可能です。

  • sync.WaitGroupatomicパッケージ: Goの並行処理において、ゴルーチンの同期やアトミックな操作を行うためのツールです。

    • sync.WaitGroup: 複数のゴルーチンの完了を待つために使用されます。
    • sync/atomicパッケージ: 共有変数へのアトミックな操作(読み書き、加算など)を提供し、データ競合を防ぎます。

技術的詳細

このコミットは、主に以下の2つの技術的側面からデータ競合の修正とReaderの挙動の調整を行っています。

  1. strings.Readerおよびbytes.ReaderにおけるprevRuneの書き込みタイミングの変更: 以前のコミット(77580046)では、Readメソッドが呼び出された際に、読み取るバイト数が0であってもr.prevRune = -1という操作が行われるように変更されていました。prevRuneUnreadRuneメソッドが正しく動作するために必要な内部状態であり、通常はReadが実際にバイトを読み込んだ後に更新されます。しかし、ゼロバイト読み取り時にもprevRuneが書き込まれるようになったことで、複数のゴルーチンが同時にRead([]byte{})(空のスライスを渡して呼び出す)のような操作を行った場合に、prevRuneへの競合書き込みが発生する可能性がありました。

    このコミットでは、src/pkg/bytes/reader.gosrc/pkg/strings/reader.goにおいて、r.prevRune = -1の行をif len(b) == 0 { return 0, nil }のチェックのに移動させました。これにより、Readメソッドが空のスライスを受け取ってすぐに0, nilを返す場合、prevRuneへの書き込みは行われなくなります。これは、ゼロバイト読み取り時にはprevRuneを更新する必要がないという元のセマンティクスに戻すことで、不要なメモリ書き込みとそれに伴うデータ競合のリスクを排除します。

  2. net/httpサーバーにおけるeofReaderの実装変更とテストの追加: net/httpサーバーは、リクエストボディが空の場合に内部的にeofReaderという特別なio.Readerを使用します。このeofReaderは常にio.EOFを返すように設計されています。以前のeofReader*strings.Readerを埋め込んでいましたが、このstrings.Readerが前述のゼロバイト読み取り時のprevRune書き込み挙動を持っていたため、net/httpサーバーの内部でデータ競合を引き起こす可能性がありました。

    このコミットでは、src/pkg/net/http/server.goにおいて、eofReaderの定義を変更しました。具体的には、*strings.Readerの埋め込みを削除し、代わりにeofReaderWithWriteToという新しい内部型を埋め込むようにしました。eofReaderWithWriteToは、io.WriterToインターフェースとio.Readerインターフェースを明示的に実装しており、Readメソッドは常に0, io.EOFを返します。これにより、eofReaderstrings.Readerの内部状態に依存することなく、独立して動作するようになり、データ競合の原因が取り除かれました。また、io.Copyがバッファを必要としないようにio.WriterToを実装していることをvar _ io.WriterTo = eofReaderという形で検証しています。

    さらに、src/pkg/net/http/serve_test.goにはTestServerEmptyBodyRaceという新しいテストが追加されました。このテストは、複数のゴルーチンから同時にHTTPリクエストを送信し、サーバーが空のボディを適切に処理し、データ競合が発生しないことをatomic操作とsync.WaitGroupを用いて検証します。これにより、修正が正しく機能していることが保証されます。

これらの変更により、GoのHTTPサーバーの堅牢性が向上し、strings.Readerbytes.ReaderReadメソッドがより予測可能な挙動を示すようになりました。

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

このコミットにおけるコアとなるコードの変更は、主に以下のファイルと箇所に集中しています。

  1. src/pkg/bytes/reader.go および src/pkg/strings/reader.go: Readメソッド内のr.prevRune = -1の行が移動されました。 変更前:

    func (r *Reader) Read(b []byte) (n int, err error) {
    	r.prevRune = -1 // ここにあった
    	if len(b) == 0 {
    		return 0, nil
    	}
    	// ...
    }
    

    変更後:

    func (r *Reader) Read(b []byte) (n int, err error) {
    	if len(b) == 0 {
    		return 0, nil
    	}
    	r.prevRune = -1 // ここに移動
    	// ...
    }
    
  2. src/pkg/net/http/server.go: eofReaderの定義が変更されました。 変更前:

    var eofReader = &struct {
    	*strings.Reader
    	io.Closer
    }{
    	strings.NewReader(""),
    	ioutil.NopCloser(nil),
    }
    

    変更後:

    type eofReaderWithWriteTo struct{}
    
    func (eofReaderWithWriteTo) WriteTo(io.Writer) (int64, error) { return 0, nil }
    func (eofReaderWithWriteTo) Read([]byte) (int, error)         { return 0, io.EOF }
    
    var eofReader = &struct {
    	eofReaderWithWriteTo
    	io.Closer
    }{
    	eofReaderWithWriteTo{},
    	ioutil.NopCloser(nil),
    }
    // Verify that an io.Copy from an eofReader won't require a buffer.
    var _ io.WriterTo = eofReader
    
  3. テストファイルの追加と修正:

    • src/pkg/bytes/reader_test.go および src/pkg/strings/reader_test.go: TestEmptyReaderConcurrentという新しいテスト関数が追加され、空のRead呼び出しが並行して行われた場合のデータ競合がないことを検証します。 UnreadRuneErrorTests内のReadテストケースで、r.Read([]byte{})r.Read([]byte{0})に変更されました。これは、ゼロバイトスライスと1バイトスライス(ただし読み込まれるのは0バイト)の違いを明確にするためです。
    • src/pkg/net/http/serve_test.go: TestServerEmptyBodyRaceという新しいテスト関数が追加され、net/httpサーバーが空のリクエストボディを並行して処理する際のデータ競合を検証します。

コアとなるコードの解説

  1. strings.Readerおよびbytes.ReaderReadメソッドにおけるprevRuneの書き込みタイミング: この変更は、Readメソッドが空のバイトスライス(len(b) == 0)を受け取った場合に、内部状態であるr.prevRuneを更新しないようにするためのものです。prevRuneUnreadRuneメソッドが正しく機能するために、直前に読み込んだルーンのサイズを記憶するフィールドです。Readが実際にバイトを読み込まない場合(つまりn=0の場合)、prevRuneを更新する必要はありません。以前のコミットでゼロバイト読み取り時にもprevRuneが書き込まれるようになったことで、複数のゴルーチンが同時にRead([]byte{})を呼び出すと、このprevRuneへの書き込みがデータ競合を引き起こす可能性がありました。この修正により、不要な書き込みが排除され、Readerの並行利用時の安全性が向上しました。

  2. net/http/server.goにおけるeofReaderの再実装: net/httpサーバーは、クライアントからのリクエストボディが空である場合に、内部的にeofReaderという特別なio.Readerを使用します。このeofReaderは、常に読み込みが終了したことを示すio.EOFを返す必要があります。 以前のeofReader*strings.Readerを埋め込んでいましたが、これはstrings.Readerの内部状態(特にprevRune)に依存していました。前述のstrings.Readerの挙動変更と相まって、この依存関係がnet/httpサーバー内部でのデータ競合の原因となっていました。 新しい実装では、eofReaderWithWriteToという専用の型を導入し、この型がio.Readerio.WriterToインターフェースを明示的に実装します。Readメソッドは常に0, io.EOFを返し、WriteToメソッドは常に0, nilを返します。これにより、eofReaderstrings.Readerの内部実装から完全に独立し、データ競合のリスクが排除されました。また、io.CopyWriteToを介して効率的に動作することも保証されます。

  3. テストの追加: 追加されたテストは、これらの修正が正しく機能していることを検証するためのものです。

    • TestEmptyReaderConcurrent: bytes.Readerstrings.Readerに対して、複数のゴルーチンから同時に空のRead呼び出しを行い、データ競合が発生しないことをGoの競合検出器(race detector)で確認します。これは、prevRuneの書き込みタイミング変更の有効性を検証します。
    • TestServerEmptyBodyRace: net/httpサーバーに対して、複数のゴルーチンから同時に空のリクエストボディを持つHTTPリクエストを送信し、サーバーがデータ競合を起こさずにこれらを処理できることを検証します。これは、eofReaderの変更とnet/httpサーバー全体の堅牢性を検証します。

これらの変更は、Goの標準ライブラリの堅牢性と並行処理の安全性を高める上で重要な役割を果たしています。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント: ioパッケージ, net/httpパッケージ, stringsパッケージ, bytesパッケージ
  • Go言語のソースコード: 上記の変更されたファイル
  • Go言語のIssueトラッカー: https://github.com/golang/go/issues
  • Go言語のCode Reviewサイト: https://golang.org/cl/
  • Go言語におけるデータ競合と競合検出器に関する一般的な情報源。I have generated the detailed explanation of the commit as requested, following all the specified instructions and including all the required sections in Japanese. The output is in Markdown format and is printed to standard output only.# [インデックス 19228] ファイルの概要

このコミットは、Go言語の標準ライブラリであるnet/httpstringsbytesパッケージにおける重要な修正を含んでいます。主な目的は、net/httpパッケージで発生していたデータ競合(data race)のバグを修正すること、そしてstrings.Readerおよびbytes.ReaderReadメソッドの挙動の一部を元に戻すことです。特に、ゼロバイトを読み取る際のメモリ書き込みの有無に関する変更が対象となっています。

コミット

commit 13ea1fd233465bc5dd410c8c64c8120ab249ab69
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date:   Fri Apr 25 06:44:51 2014 -0700

    net/http, strings, bytes: fix http race, revert part of Reader behavior change
    
    I fixed this data race regression in two ways: in net/http itself, and also
    partially reverting the change from https://golang.org/cl/77580046 .
    Previously a Read from a strings.Reader or bytes.Reader returning 0 bytes
    would not be a memory write. After 77580046 it was. This reverts that back
    in case others depended on that. Also adds tests.
    
    Fixes #7856
    
    LGTM=ruiu, iant
    R=iant, ruiu
    CC=golang-codereviews, gri
    https://golang.org/cl/94740044

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

https://github.com/golang/go/commit/13ea1fd233465bc5dd410c8c64c8120ab249ab69

元コミット内容

net/http, strings, bytes: fix http race, revert part of Reader behavior change

このコミットは、net/httpにおけるHTTPの競合状態を修正し、Readerの挙動変更の一部を元に戻します。

このデータ競合の回帰は2つの方法で修正されました。1つはnet/http自体で、もう1つはhttps://golang.org/cl/77580046からの変更を部分的に元に戻すことです。以前は、strings.Readerまたはbytes.ReaderからのReadが0バイトを返してもメモリ書き込みは発生しませんでした。しかし、77580046の変更後には発生するようになりました。このコミットは、他のコードがその挙動に依存している可能性を考慮し、元の挙動に戻します。また、テストも追加されています。

Issue #7856 を修正します。

変更の背景

このコミットの背景には、主に以下の2つの問題がありました。

  1. net/httpパッケージにおけるデータ競合(Data Race)の発生: Goのnet/httpパッケージのサーバー実装において、特定の条件下でデータ競合が発生するバグ(Issue #7856)が報告されました。この競合は、特にリクエストボディが空の場合に顕著であったと推測されます。データ競合は、複数のゴルーチンが同時に同じメモリ領域にアクセスし、少なくとも1つが書き込みを行う場合に発生し、予測不能な動作やクラッシュを引き起こす可能性があります。

  2. strings.Readerおよびbytes.ReaderReadメソッドの意図しない挙動変更: 以前のコミット(https://golang.org/cl/77580046)によって、strings.Readerbytes.ReaderReadメソッドが、たとえ読み取るバイト数が0であっても、内部状態(具体的にはprevRuneフィールド)にメモリ書き込みを行うように変更されました。この変更自体は、おそらくprevRuneの確実なリセットを意図したものだったと考えられます。しかし、この「ゼロバイト読み取り時のメモリ書き込み」という新しい挙動が、上記net/httpのデータ競合の一因となっていた、あるいは他の既存コードの前提を崩す可能性がありました。特に、複数のゴルーチンから同時にReadが呼ばれ、そのうちのいくつかがゼロバイト読み取りを行う場合に、prevRuneへの書き込みが競合を引き起こす可能性があったのです。

このコミットは、これら2つの問題を同時に解決しようとするものです。net/http側の修正と、Readerの挙動を部分的に元に戻すことで、より堅牢なシステムを目指しています。

前提知識の解説

このコミットを理解するためには、以下のGo言語の概念と関連技術についての知識が役立ちます。

  • Go言語の並行処理とゴルーチン (Goroutines): Go言語は、軽量なスレッドであるゴルーチンを用いて並行処理を容易に記述できます。ゴルーチンはOSのスレッドよりもはるかに軽量で、数千、数万のゴルーチンを同時に実行することが可能です。

  • データ競合 (Data Race): 複数のゴルーチンが同時に同じメモリ領域にアクセスし、そのうち少なくとも1つが書き込みを行う場合に発生するバグです。データ競合が発生すると、プログラムの動作が非決定論的になり、デバッグが非常に困難になります。Goにはgo run -raceというデータ競合検出ツールがあり、開発中に競合を特定するのに役立ちます。

  • io.Readerインターフェース: Go言語におけるデータの読み込み操作を抽象化する基本的なインターフェースです。 type Reader interface { Read(p []byte) (n int, err error) } Readメソッドは、pに最大len(p)バイトを読み込み、読み込んだバイト数nとエラーerrを返します。nが0でerrnilの場合、それはReadが何も読み込まなかったが、エラーでもないことを意味します(例えば、非ブロックI/Oでデータがまだ利用できない場合など)。nが0でerrio.EOFの場合、それはストリームの終端に達したことを意味します。

  • strings.Readerbytes.Reader: それぞれ文字列(string)とバイトスライス([]byte)をio.Readerインターフェースとして扱うための型です。これらは、メモリ上のデータをファイルのように読み込む際に便利です。内部的には、読み込み位置を示すインデックス(i)と、UnreadRuneメソッドのために直前のルーンのサイズを記憶するprevRuneなどのフィールドを持っています。

  • net/httpパッケージ: Go言語でHTTPクライアントおよびサーバーを構築するための標準ライブラリです。サーバーは複数のクライアントからのリクエストを並行して処理するため、内部でゴルーチンを多用します。そのため、データ競合が発生しやすい環境でもあります。

  • io.Copyio.WriterToインターフェース: io.Copy(dst io.Writer, src io.Reader)関数は、srcからdstへデータをコピーします。この関数は、srcio.WriterToインターフェースを実装している場合、そのWriteToメソッドを優先的に使用します。WriteToメソッドは、バッファを介さずに直接データを書き込むことができるため、効率的なコピーが可能です。

  • sync.WaitGroupatomicパッケージ: Goの並行処理において、ゴルーチンの同期やアトミックな操作を行うためのツールです。

    • sync.WaitGroup: 複数のゴルーチンの完了を待つために使用されます。
    • sync/atomicパッケージ: 共有変数へのアトミックな操作(読み書き、加算など)を提供し、データ競合を防ぎます。

技術的詳細

このコミットは、主に以下の2つの技術的側面からデータ競合の修正とReaderの挙動の調整を行っています。

  1. strings.Readerおよびbytes.ReaderにおけるprevRuneの書き込みタイミングの変更: 以前のコミット(77580046)では、Readメソッドが呼び出された際に、読み取るバイト数が0であってもr.prevRune = -1という操作が行われるように変更されていました。prevRuneUnreadRuneメソッドが正しく動作するために必要な内部状態であり、通常はReadが実際にバイトを読み込んだ後に更新されます。しかし、ゼロバイト読み取り時にもprevRuneが書き込まれるようになったことで、複数のゴルーチンが同時にRead([]byte{})(空のスライスを渡して呼び出す)のような操作を行った場合に、prevRuneへの競合書き込みが発生する可能性がありました。

    このコミットでは、src/pkg/bytes/reader.gosrc/pkg/strings/reader.goにおいて、r.prevRune = -1の行をif len(b) == 0 { return 0, nil }のチェックのに移動させました。これにより、Readメソッドが空のスライスを受け取ってすぐに0, nilを返す場合、prevRuneへの書き込みは行われなくなります。これは、ゼロバイト読み取り時にはprevRuneを更新する必要がないという元のセマンティクスに戻すことで、不要なメモリ書き込みとそれに伴うデータ競合のリスクを排除します。

  2. net/httpサーバーにおけるeofReaderの実装変更とテストの追加: net/httpサーバーは、リクエストボディが空の場合に内部的にeofReaderという特別なio.Readerを使用します。このeofReaderは常にio.EOFを返すように設計されています。以前のeofReader*strings.Readerを埋め込んでいましたが、このstrings.Readerが前述のゼロバイト読み取り時のprevRune書き込み挙動を持っていたため、net/httpサーバーの内部でデータ競合を引き起こす可能性がありました。

    このコミットでは、src/pkg/net/http/server.goにおいて、eofReaderの定義を変更しました。具体的には、*strings.Readerの埋め込みを削除し、代わりにeofReaderWithWriteToという新しい内部型を埋め込むようにしました。eofReaderWithWriteToは、io.WriterToインターフェースとio.Readerインターフェースを明示的に実装しており、Readメソッドは常に0, io.EOFを返します。これにより、eofReaderstrings.Readerの内部状態に依存することなく、独立して動作するようになり、データ競合の原因が取り除かれました。また、io.Copyがバッファを必要としないようにio.WriterToを実装していることをvar _ io.WriterTo = eofReaderという形で検証しています。

    さらに、src/pkg/net/http/serve_test.goにはTestServerEmptyBodyRaceという新しいテストが追加されました。このテストは、複数のゴルーチンから同時にHTTPリクエストを送信し、サーバーが空のボディを適切に処理し、データ競合が発生しないことをatomic操作とsync.WaitGroupを用いて検証します。これにより、修正が正しく機能していることが保証されます。

これらの変更により、GoのHTTPサーバーの堅牢性が向上し、strings.Readerbytes.ReaderReadメソッドがより予測可能な挙動を示すようになりました。

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

このコミットにおけるコアとなるコードの変更は、主に以下のファイルと箇所に集中しています。

  1. src/pkg/bytes/reader.go および src/pkg/strings/reader.go: Readメソッド内のr.prevRune = -1の行が移動されました。 変更前:

    func (r *Reader) Read(b []byte) (n int, err error) {
    	r.prevRune = -1 // ここにあった
    	if len(b) == 0 {
    		return 0, nil
    	}
    	// ...
    }
    

    変更後:

    func (r *Reader) Read(b []byte) (n int, err error) {
    	if len(b) == 0 {
    		return 0, nil
    	}
    	r.prevRune = -1 // ここに移動
    	// ...
    }
    
  2. src/pkg/net/http/server.go: eofReaderの定義が変更されました。 変更前:

    var eofReader = &struct {
    	*strings.Reader
    	io.Closer
    }{
    	strings.NewReader(""),
    	ioutil.NopCloser(nil),
    }
    

    変更後:

    type eofReaderWithWriteTo struct{}
    
    func (eofReaderWithWriteTo) WriteTo(io.Writer) (int64, error) { return 0, nil }
    func (eofReaderWithWriteTo) Read([]byte) (int, error)         { return 0, io.EOF }
    
    var eofReader = &struct {
    	eofReaderWithWriteTo
    	io.Closer
    }{
    	eofReaderWithWriteTo{},
    	ioutil.NopCloser(nil),
    }
    // Verify that an io.Copy from an eofReader won't require a buffer.
    var _ io.WriterTo = eofReader
    
  3. テストファイルの追加と修正:

    • src/pkg/bytes/reader_test.go および src/pkg/strings/reader_test.go: TestEmptyReaderConcurrentという新しいテスト関数が追加され、空のRead呼び出しが並行して行われた場合のデータ競合がないことを検証します。 UnreadRuneErrorTests内のReadテストケースで、r.Read([]byte{})r.Read([]byte{0})に変更されました。これは、ゼロバイトスライスと1バイトスライス(ただし読み込まれるのは0バイト)の違いを明確にするためです。
    • src/pkg/net/http/serve_test.go: TestServerEmptyBodyRaceという新しいテスト関数が追加され、net/httpサーバーが空のリクエストボディを並行して処理する際のデータ競合を検証します。

コアとなるコードの解説

  1. strings.Readerおよびbytes.ReaderReadメソッドにおけるprevRuneの書き込みタイミング: この変更は、Readメソッドが空のバイトスライス(len(b) == 0)を受け取った場合に、内部状態であるr.prevRuneを更新しないようにするためのものです。prevRuneUnreadRuneメソッドが正しく機能するために、直前に読み込んだルーンのサイズを記憶するフィールドです。Readが実際にバイトを読み込まない場合(つまりn=0の場合)、prevRuneを更新する必要はありません。以前のコミットでゼロバイト読み取り時にもprevRuneが書き込まれるようになったことで、複数のゴルーチンが同時にRead([]byte{})を呼び出すと、このprevRuneへの書き込みがデータ競合を引き起こす可能性がありました。この修正により、不要な書き込みが排除され、Readerの並行利用時の安全性が向上しました。

  2. net/http/server.goにおけるeofReaderの再実装: net/httpサーバーは、クライアントからのリクエストボディが空である場合に、内部的にeofReaderという特別なio.Readerを使用します。このeofReaderは、常に読み込みが終了したことを示すio.EOFを返す必要があります。 以前のeofReader*strings.Readerを埋め込んでいましたが、これはstrings.Readerの内部状態(特にprevRune)に依存していました。前述のstrings.Readerの挙動変更と相まって、この依存関係がnet/httpサーバー内部でのデータ競合の原因となっていました。 新しい実装では、eofReaderWithWriteToという専用の型を導入し、この型がio.Readerio.WriterToインターフェースを明示的に実装します。Readメソッドは常に0, io.EOFを返し、WriteToメソッドは常に0, nilを返します。これにより、eofReaderstrings.Readerの内部実装から完全に独立し、データ競合のリスクが排除されました。また、io.CopyWriteToを介して効率的に動作することも保証されます。

  3. テストの追加: 追加されたテストは、これらの修正が正しく機能していることを検証するためのものです。

    • TestEmptyReaderConcurrent: bytes.Readerstrings.Readerに対して、複数のゴルーチンから同時に空のRead呼び出しを行い、データ競合が発生しないことをGoの競合検出器(race detector)で確認します。これは、prevRuneの書き込みタイミング変更の有効性を検証します。
    • TestServerEmptyBodyRace: net/httpサーバーに対して、複数のゴルーチンから同時に空のリクエストボディを持つHTTPリクエストを送信し、サーバーがデータ競合を起こさずにこれらを処理できることを検証します。これは、eofReaderの変更とnet/httpサーバー全体の堅牢性を検証します。

これらの変更は、Goの標準ライブラリの堅牢性と並行処理の安全性を高める上で重要な役割を果たします。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント: ioパッケージ, net/httpパッケージ, stringsパッケージ, bytesパッケージ
  • Go言語のソースコード: 上記の変更されたファイル
  • Go言語のIssueトラッカー: https://github.com/golang/go/issues
  • Go言語のCode Reviewサイト: https://golang.org/cl/
  • Go言語におけるデータ競合と競合検出器に関する一般的な情報源。