[インデックス 11375] ファイルの概要
このコミットは、Go言語の標準ライブラリである archive/tar
パッケージ内の reader_test.go
ファイルに対する変更です。このファイルは、archive/tar
パッケージのリーダー機能、特に非シーク可能な(non-seekable)ストリームからの読み込みに関するテストケースを定義しています。具体的には、TestNonSeekable
というテスト関数における競合状態(race condition)の修正が目的です。
コミット
このコミットは、archive/tar
パッケージの TestNonSeekable
テスト関数における競合状態を修正します。以前の実装では、os.Pipe
を使用してファイルの内容をパイプ経由で読み込むゴルーチンと、テスト関数が defer
を使ってファイルを閉じる処理との間にタイミングの問題がありました。これにより、ゴルーチンが io.EOF
を受け取る前にファイルが閉じられ、EBADF
エラーや SIGPIPE
シグナルが発生し、特に FreeBSD や OpenBSD 環境でテストが失敗する原因となっていました。この修正では、adg@golang.org
のコードを基にテストを再実装し、os.Pipe
を介したデータ転送を排除することで、競合状態を根本的に解消しています。
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/e3e1804ed2af1163335369300cfc562c35ffa4c9
元コミット内容
commit e3e1804ed2af1163335369300cfc562c35ffa4c9
Author: Joel Sing <jsing@google.com>
Date: Wed Jan 25 13:44:53 2012 +1100
archive/tar: fix race in TestNonSeekable
Reimplement the test based on code from adg@golang.org.
The previous version has a race since the file is closed via defer
rather than in the go routine. This meant that the file could be
closed before the go routine has actually received io.EOF. It then
receives EBADF and continues to do zero-byte writes to the pipe.
This addresses an issue seen on FreeBSD and OpenBSD, where the test
passes but exits with a SIGPIPE, resulting in a failure.
R=golang-dev, adg
CC=golang-dev
https://golang.org/cl/5554083
---
src/pkg/archive/tar/reader_test.go | 27 ++++++++-------------------
1 file changed, 8 insertions(+), 19 deletions(-)
diff --git a/src/pkg/archive/tar/reader_test.go b/src/pkg/archive/tar/reader_test.go
index 0a6513d0ca..0a8646c393 100644
--- a/src/pkg/archive/tar/reader_test.go
+++ b/src/pkg/archive/tar/reader_test.go
@@ -240,31 +240,20 @@ func TestNonSeekable(t *testing.T) {
}\n \tdefer f.Close()\n \n-\t// pipe the data in\n-\tr, w, err := os.Pipe()\n-\tif err != nil {\n-\t\tt.Fatalf(\"Unexpected error %s\", err)\n+\ttype readerOnly struct {\n+\t\tio.Reader\n \t}\n-\tgo func() {\n-\t\trdbuf := make([]uint8, 1<<16)\n-\t\tfor {\n-\t\t\tnr, err := f.Read(rdbuf)\n-\t\t\tw.Write(rdbuf[0:nr])\n-\t\t\tif err == io.EOF {\n-\t\t\t\tbreak\n-\t\t\t}\n-\t\t}\n-\t\tw.Close()\n-\t}()\n-\n-\ttr := NewReader(r)\n+\ttr := NewReader(readerOnly{f})\n \tnread := 0\n \n \tfor ; ; nread++ {\n-\t\thdr, err := tr.Next()\n-\t\tif hdr == nil || err == io.EOF {\n+\t\t_, err := tr.Next()\n+\t\tif err == io.EOF {\n \t\t\tbreak\n \t\t}\n+\t\tif err != nil {\n+\t\t\tt.Fatalf(\"Unexpected error: %v\", err)\n+\t\t}\n \t}\n \n \tif nread != len(test.headers) {\n```
## 変更の背景
この変更の背景には、`archive/tar` パッケージの `TestNonSeekable` テストが特定のオペレーティングシステム(特に FreeBSD と OpenBSD)で不安定な動作を示し、`SIGPIPE` シグナルによってテストが失敗するという問題がありました。
元の `TestNonSeekable` テストは、非シーク可能なリーダー(`io.Reader` インターフェースのみを実装し、`io.Seeker` インターフェースを実装しないリーダー)からの `tar` アーカイブの読み込みを検証することを目的としていました。このテストでは、`os.Pipe()` を使用して、ファイル `f` から読み込んだデータをパイプの書き込み側 `w` に書き込み、パイプの読み込み側 `r` から `tar.NewReader` がデータを読み込むという間接的なデータフローを構築していました。
問題は、このパイプへのデータ書き込みを別のゴルーチンで行い、元のファイル `f` のクローズ処理を `defer f.Close()` で行っていた点にありました。`defer` は関数が終了する直前に実行されるため、パイプへの書き込みを行うゴルーチンが `io.EOF` を完全に処理し終える前に、元のファイル `f` が閉じられてしまう可能性がありました。
具体的には、以下の競合状態が発生していました。
1. ゴルーチンがファイル `f` からデータを読み込み、パイプ `w` に書き込む。
2. `tar.NewReader` がパイプ `r` からデータを読み込む。
3. ファイル `f` の終端に達し、ゴルーチンが `io.EOF` を受け取る。
4. ゴルーチンが `w.Close()` を呼び出す。
5. しかし、`TestNonSeekable` 関数自体が終了する際に `defer f.Close()` が実行され、これがゴルーチンが `io.EOF` を完全に処理し、`w.Close()` を呼び出すよりも早く実行されることがあった。
6. もし `f` がゴルーチンによる読み取り中に閉じられてしまうと、ゴルーチンは無効なファイルディスクリプタに対して読み取りを試み、`EBADF` (Bad File Descriptor) エラーを受け取る。
7. さらに、パイプの読み込み側が閉じられた後も、書き込み側がデータを書き込もうとすると、オペレーティングシステムは `SIGPIPE` シグナルを送信します。これは、パイプの読み込み側が既に閉じられているにもかかわらず、書き込み側が書き込みを継続しようとした場合に発生する一般的な UNIX 系システムの挙動です。この `SIGPIPE` シグナルが捕捉されない場合、プログラムは異常終了します。
この競合状態は、特にタイミングがシビアな環境(FreeBSD や OpenBSD など)で顕在化し、テストの不安定性や失敗を引き起こしていました。このコミットは、この不安定性を解消し、テストの信頼性を向上させることを目的としています。
## 前提知識の解説
このコミットの理解には、以下のGo言語およびOS関連の概念の知識が役立ちます。
* **`io.Reader` インターフェース**: Go言語における基本的な入力操作を抽象化するインターフェースです。`Read(p []byte) (n int, err error)` メソッドを持ち、データをバイトスライス `p` に読み込み、読み込んだバイト数 `n` とエラー `err` を返します。データがこれ以上ない場合は `io.EOF` エラーを返します。
* **`io.Writer` インターフェース**: Go言語における基本的な出力操作を抽象化するインターフェースです。`Write(p []byte) (n int, err error)` メソッドを持ち、バイトスライス `p` のデータを書き込み、書き込んだバイト数 `n` とエラー `err` を返します。
* **`io.EOF`**: `io` パッケージで定義されているエラー定数で、入力の終わりに達したことを示します。
* **`os.Pipe()`**: `os` パッケージの関数で、同期的なインメモリパイプを作成します。`io.Reader` と `io.Writer` のペアを返します。`r` から読み込まれたデータは `w` に書き込まれたデータです。これはプロセス間通信や、今回のケースのようにストリーム処理のテストでよく使われます。
* **`defer` ステートメント**: Go言語のキーワードで、`defer` に続く関数呼び出しを、その関数がリターンする直前(パニックが発生した場合も含む)に実行するようにスケジュールします。リソースのクリーンアップ(ファイルのクローズ、ロックの解放など)によく使用されます。
* **ゴルーチン (Goroutine)**: Go言語の軽量な並行処理の単位です。`go` キーワードを使って関数呼び出しの前に置くことで、その関数を新しいゴルーチンで実行します。ゴルーチンはOSのスレッドよりもはるかに軽量で、数千、数万のゴルーチンを同時に実行することが可能です。
* **競合状態 (Race Condition)**: 複数の並行プロセス(この場合はゴルーチンとメインのテスト関数)が共有リソース(この場合はファイルディスクリプタ `f`)にアクセスし、そのアクセス順序によって結果が非決定的に変わる状態を指します。今回のケースでは、ファイル `f` のクローズと、そのファイルから読み取るゴルーチンの処理完了のタイミングが競合していました。
* **`SIGPIPE` シグナル**: UNIX系オペレーティングシステムで発生するシグナルの一つです。パイプの読み込み側が閉じられた後に、書き込み側がそのパイプにデータを書き込もうとした場合に、書き込み側のプロセスに送信されます。デフォルトではプロセスを終了させます。
* **`EBADF` (Bad File Descriptor)**: 無効なファイルディスクリプタに対して操作を行おうとした際に返されるエラーコードです。ファイルが既に閉じられている、または存在しないファイルディスクリプタを参照している場合に発生します。
## 技術的詳細
元の `TestNonSeekable` テストは、`os.Pipe()` を利用して、`tar.NewReader` に非シーク可能なストリームを提供していました。これは、`f` というファイルからデータを読み込み、それを `w` (パイプの書き込み側) に書き込むゴルーチンを起動し、`tar.NewReader` には `r` (パイプの読み込み側) を渡すという構造でした。
```go
// 変更前のコードの抜粋
r, w, err := os.Pipe()
// ...
go func() {
rdbuf := make([]uint8, 1<<16)
for {
nr, err := f.Read(rdbuf) // f から読み込み
w.Write(rdbuf[0:nr]) // w に書き込み
if err == io.EOF {
break
}
}
w.Close() // ゴルーチン内でパイプの書き込み側を閉じる
}()
tr := NewReader(r) // tar.NewReader にパイプの読み込み側を渡す
// ...
defer f.Close() // テスト関数が終了する際に f を閉じる
この設計における競合状態の核心は、defer f.Close()
とゴルーチン内の w.Close()
の実行タイミングにありました。
f.Close()
の早期実行:defer f.Close()
はTestNonSeekable
関数がリターンする直前に実行されます。しかし、ゴルーチンがf
からio.EOF
を受け取り、w.Close()
を呼び出す処理は非同期で行われます。もしTestNonSeekable
関数がゴルーチンがio.EOF
を完全に処理し終える前にリターンし始めると(例えば、tr.Next()
がエラーを返してループを抜けた場合など)、f.Close()
が先に実行されてしまう可能性がありました。EBADF
エラー:f
が閉じられた後も、ゴルーチンがf.Read()
を呼び出そうとすると、無効なファイルディスクリプタに対して操作を行おうとするため、EBADF
エラーが発生します。SIGPIPE
シグナル: さらに、tar.NewReader
がパイプr
から読み込みを終えてパイプの読み込み側が閉じられた後も、ゴルーチンがw.Write()
を呼び出し続けると、OSはSIGPIPE
シグナルを送信します。これは、パイプの読み込み側が既に閉じられているにもかかわらず、書き込み側が書き込みを継続しようとした場合に発生します。このシグナルが捕捉されない場合、テストプロセスは異常終了します。
この問題を解決するため、コミットでは os.Pipe()
と中間ゴルーチンを完全に排除し、より直接的なアプローチを採用しました。
// 変更後のコードの抜粋
type readerOnly struct {
io.Reader
}
tr := NewReader(readerOnly{f}) // tar.NewReader に直接 f をラップしたものを渡す
新しいアプローチでは、readerOnly
というシンプルな構造体を定義し、これに元のファイル f
を io.Reader
インターフェースとして埋め込みます。readerOnly
は io.Reader
インターフェースを実装しているため、tar.NewReader
はこれを直接受け入れることができます。
この変更により、以下の利点が得られます。
- 競合状態の解消:
os.Pipe()
とそれに関連するゴルーチンが不要になるため、f.Close()
とパイプへの書き込みのタイミングに関する競合状態がなくなります。tar.NewReader
は直接f
から読み込むため、f
のクローズはtar.NewReader
がio.EOF
を受け取った後に安全に行われます。 - コードの簡素化: パイプの設定とゴルーチンの管理が不要になり、テストコードが大幅に簡素化されます。
SIGPIPE
の回避: パイプへの書き込みがなくなるため、SIGPIPE
シグナルが発生する可能性がなくなります。
この修正は、テストの信頼性を高め、特定のOS環境での不安定な失敗を解消する上で非常に効果的です。
コアとなるコードの変更箇所
--- a/src/pkg/archive/tar/reader_test.go
+++ b/src/pkg/archive/tar/reader_test.go
@@ -240,31 +240,20 @@ func TestNonSeekable(t *testing.T) {
}\n \tdefer f.Close()\n \n-\t// pipe the data in\n-\tr, w, err := os.Pipe()\n-\tif err != nil {\n-\t\tt.Fatalf(\"Unexpected error %s\", err)\n+\ttype readerOnly struct {\n+\t\tio.Reader\n \t}\n-\tgo func() {\n-\t\trdbuf := make([]uint8, 1<<16)\n-\t\tfor {\n-\t\t\tnr, err := f.Read(rdbuf)\n-\t\t\tw.Write(rdbuf[0:nr])\n-\t\t\tif err == io.EOF {\n-\t\t\t\tbreak\n-\t\t\t}\n-\t\t}\n-\t\tw.Close()\n-\t}()\n-\n-\ttr := NewReader(r)\n+\ttr := NewReader(readerOnly{f})\n \tnread := 0\n \n \tfor ; ; nread++ {\n-\t\thdr, err := tr.Next()\n-\t\tif hdr == nil || err == io.EOF {\n+\t\t_, err := tr.Next()\n+\t\tif err == io.EOF {\n \t\t\tbreak\n \t\t}\n+\t\tif err != nil {\n+\t\t\tt.Fatalf(\"Unexpected error: %v\", err)\n+\t\t}\n \t}\n \n \tif nread != len(test.headers) {\n```
## コアとなるコードの解説
変更の核心は、`os.Pipe()` を使用した間接的なデータ転送メカニズムを削除し、代わりに `io.Reader` インターフェースを直接利用する点にあります。
1. **`os.Pipe()` とゴルーチンの削除**:
変更前は、`os.Pipe()` で作成したパイプの書き込み側 `w` にファイル `f` から読み込んだデータをゴルーチンで書き込み、パイプの読み込み側 `r` を `tar.NewReader` に渡していました。この複雑な設定が競合状態の原因でした。
変更後、この部分のコード(`r, w, err := os.Pipe()` から `w.Close()` までのゴルーチン全体)が削除されました。
2. **`readerOnly` 構造体の導入**:
新しく `readerOnly` という構造体が定義されました。
```go
type readerOnly struct {
io.Reader
}
```
この構造体は、`io.Reader` インターフェースを匿名フィールドとして埋め込んでいます。Go言語の埋め込み(embedding)の特性により、`readerOnly` 型のインスタンスは、埋め込まれた `io.Reader` のすべてのメソッド(この場合は `Read` メソッド)を自動的に継承します。これにより、`readerOnly` 型自体が `io.Reader` インターフェースを実装しているとみなされます。
3. **`tar.NewReader` への直接的な `io.Reader` の提供**:
変更前は `tr := NewReader(r)` と、パイプの読み込み側 `r` を `tar.NewReader` に渡していました。
変更後、`tr := NewReader(readerOnly{f})` となりました。これは、元のファイル `f`(これも `io.Reader` インターフェースを実装しています)を `readerOnly` 構造体でラップし、その `readerOnly` インスタンスを直接 `tar.NewReader` に渡しています。
`tar.NewReader` は `io.Reader` を引数として受け取るため、`readerOnly{f}` は有効な引数となります。これにより、`tar.NewReader` は直接 `f` からデータを読み込むようになります。
4. **エラーハンドリングの追加**:
`tr.Next()` の呼び出し後、以前は `if hdr == nil || err == io.EOF` という条件でループを抜けていましたが、`hdr == nil` のチェックが削除され、`if err == io.EOF` のみでループを抜けるようになりました。
さらに、`if err != nil` のチェックが追加され、`io.EOF` 以外のエラーが発生した場合には `t.Fatalf` でテストを失敗させるようになりました。これにより、より堅牢なエラーハンドリングが実現されています。
この変更により、データフローが大幅に簡素化され、ファイルクローズとデータ読み込みの間の競合状態が根本的に解消されました。`tar.NewReader` は直接 `f` から読み込むため、`f` が閉じられるタイミングは `tar.NewReader` が `io.EOF` を受け取った後となり、安全性が確保されます。
## 関連リンク
* Go CL (Change List) 5554083: [https://golang.org/cl/5554083](https://golang.org/cl/5554083) (このコミットに対応するGoのコードレビューシステム上の変更リスト)
## 参考にした情報源リンク
* Go言語の `io` パッケージに関するドキュメント: [https://pkg.go.dev/io](https://pkg.go.dev/io)
* Go言語の `os` パッケージに関するドキュメント: [https://pkg.go.dev/os](https://pkg.go.dev/os)
* Go言語の並行処理(ゴルーチンとチャネル)に関する一般的な情報源
* UNIX系システムにおける `SIGPIPE` シグナルに関する情報源
* Go言語の `defer` ステートメントに関する情報源
* Go言語の埋め込み(embedding)に関する情報源I have extracted the commit information and formulated the explanation according to the specified structure and requirements. I have also performed a web search for the CL link to ensure all relevant context is included.
Now, I will output the generated Markdown content.
```markdown
# [インデックス 11375] ファイルの概要
このコミットは、Go言語の標準ライブラリである `archive/tar` パッケージ内の `reader_test.go` ファイルに対する変更です。このファイルは、`archive/tar` パッケージのリーダー機能、特に非シーク可能な(non-seekable)ストリームからの読み込みに関するテストケースを定義しています。具体的には、`TestNonSeekable` というテスト関数における競合状態(race condition)の修正が目的です。
## コミット
このコミットは、`archive/tar` パッケージの `TestNonSeekable` テスト関数における競合状態を修正します。以前の実装では、`os.Pipe` を使用してファイルの内容をパイプ経由で読み込むゴルーチンと、テスト関数が `defer` を使ってファイルを閉じる処理との間にタイミングの問題がありました。これにより、ゴルーチンが `io.EOF` を受け取る前にファイルが閉じられ、`EBADF` エラーや `SIGPIPE` シグナルが発生し、特に FreeBSD や OpenBSD 環境でテストが失敗する原因となっていました。この修正では、`adg@golang.org` のコードを基にテストを再実装し、`os.Pipe` を介したデータ転送を排除することで、競合状態を根本的に解消しています。
## GitHub上でのコミットページへのリンク
[https://github.com/golang/go/commit/e3e1804ed2af1163335369300cfc562c35ffa4c9](https://github.com/golang/go/commit/e3e1804ed2af1163335369300cfc562c35ffa4c9)
## 元コミット内容
commit e3e1804ed2af1163335369300cfc562c35ffa4c9 Author: Joel Sing jsing@google.com Date: Wed Jan 25 13:44:53 2012 +1100
archive/tar: fix race in TestNonSeekable
Reimplement the test based on code from adg@golang.org.
The previous version has a race since the file is closed via defer
rather than in the go routine. This meant that the file could be
closed before the go routine has actually received io.EOF. It then
receives EBADF and continues to do zero-byte writes to the pipe.
This addresses an issue seen on FreeBSD and OpenBSD, where the test
passes but exits with a SIGPIPE, resulting in a failure.
R=golang-dev, adg
CC=golang-dev
https://golang.org/cl/5554083
src/pkg/archive/tar/reader_test.go | 27 ++++++++------------------- 1 file changed, 8 insertions(+), 19 deletions(-)
diff --git a/src/pkg/archive/tar/reader_test.go b/src/pkg/archive/tar/reader_test.go index 0a6513d0ca..0a8646c393 100644 --- a/src/pkg/archive/tar/reader_test.go +++ b/src/pkg/archive/tar/reader_test.go @@ -240,31 +240,20 @@ func TestNonSeekable(t *testing.T) { }\n \tdefer f.Close()\n \n-\t// pipe the data in\n-\tr, w, err := os.Pipe()\n-\tif err != nil {\n-\t\tt.Fatalf("Unexpected error %s", err)\n+\ttype readerOnly struct {\n+\t\tio.Reader\n \t}\n-\tgo func() {\n-\t\trdbuf := make([]uint8, 1<<16)\n-\t\tfor {\n-\t\t\tnr, err := f.Read(rdbuf)\n-\t\t\tw.Write(rdbuf[0:nr])\n-\t\t\tif err == io.EOF {\n-\t\t\t\tbreak\n-\t\t\t}\n-\t\t}\n-\t\tw.Close()\n-\t}()\n-\n-\ttr := NewReader(r)\n+\ttr := NewReader(readerOnly{f})\n \tnread := 0\n \n \tfor ; ; nread++ {\n-\t\thdr, err := tr.Next()\n-\t\tif hdr == nil || err == io.EOF {\n+\t\t_, err := tr.Next()\n+\t\tif err == io.EOF {\n \t\t\tbreak\n \t\t}\n+\t\tif err != nil {\n+\t\t\tt.Fatalf("Unexpected error: %v", err)\n+\t\t}\n \t}\n \n \tif nread != len(test.headers) {\n```
変更の背景
この変更の背景には、archive/tar
パッケージの TestNonSeekable
テストが特定のオペレーティングシステム(特に FreeBSD と OpenBSD)で不安定な動作を示し、SIGPIPE
シグナルによってテストが失敗するという問題がありました。
元の TestNonSeekable
テストは、非シーク可能なリーダー(io.Reader
インターフェースのみを実装し、io.Seeker
インターフェースを実装しないリーダー)からの tar
アーカイブの読み込みを検証することを目的としていました。このテストでは、os.Pipe()
を使用して、ファイル f
から読み込んだデータをパイプの書き込み側 w
に書き込み、パイプの読み込み側 r
から tar.NewReader
がデータを読み込むという間接的なデータフローを構築していました。
問題は、このパイプへのデータ書き込みを別のゴルーチンで行い、元のファイル f
のクローズ処理を defer f.Close()
で行っていた点にありました。defer
は関数が終了する直前に実行されるため、パイプへの書き込みを行うゴルーチンが io.EOF
を完全に処理し終える前に、元のファイル f
が閉じられてしまう可能性がありました。
具体的には、以下の競合状態が発生していました。
- ゴルーチンがファイル
f
からデータを読み込み、パイプw
に書き込む。 tar.NewReader
がパイプr
からデータを読み込む。- ファイル
f
の終端に達し、ゴルーチンがio.EOF
を受け取る。 - ゴルーチンが
w.Close()
を呼び出す。 - しかし、
TestNonSeekable
関数自体が終了する際にdefer f.Close()
が実行され、これがゴルーチンがio.EOF
を完全に処理し、w.Close()
を呼び出すよりも早く実行されることがあった。 - もし
f
がゴルーチンによる読み取り中に閉じられてしまうと、ゴルーチンは無効なファイルディスクリプタに対して読み取りを試み、EBADF
(Bad File Descriptor) エラーを受け取る。 - さらに、パイプの読み込み側が閉じられた後も、書き込み側がデータを書き込もうとすると、オペレーティングシステムは
SIGPIPE
シグナルを送信します。これは、パイプの読み込み側が既に閉じられているにもかかわらず、書き込み側が書き込みを継続しようとした場合に発生する一般的な UNIX 系システムの挙動です。このSIGPIPE
シグナルが捕捉されない場合、プログラムは異常終了します。
この競合状態は、特にタイミングがシビアな環境(FreeBSD や OpenBSD など)で顕在化し、テストの不安定性や失敗を引き起こしていました。このコミットは、この不安定性を解消し、テストの信頼性を向上させることを目的としています。
前提知識の解説
このコミットの理解には、以下のGo言語およびOS関連の概念の知識が役立ちます。
io.Reader
インターフェース: Go言語における基本的な入力操作を抽象化するインターフェースです。Read(p []byte) (n int, err error)
メソッドを持ち、データをバイトスライスp
に読み込み、読み込んだバイト数n
とエラーerr
を返します。データがこれ以上ない場合はio.EOF
エラーを返します。io.Writer
インターフェース: Go言語における基本的な出力操作を抽象化するインターフェースです。Write(p []byte) (n int, err error)
メソッドを持ち、バイトスライスp
のデータを書き込み、書き込んだバイト数n
とエラーerr
を返します。io.EOF
:io
パッケージで定義されているエラー定数で、入力の終わりに達したことを示します。os.Pipe()
:os
パッケージの関数で、同期的なインメモリパイプを作成します。io.Reader
とio.Writer
のペアを返します。r
から読み込まれたデータはw
に書き込まれたデータです。これはプロセス間通信や、今回のケースのようにストリーム処理のテストでよく使われます。defer
ステートメント: Go言語のキーワードで、defer
に続く関数呼び出しを、その関数がリターンする直前(パニックが発生した場合も含む)に実行するようにスケジュールします。リソースのクリーンアップ(ファイルのクローズ、ロックの解放など)によく使用されます。- ゴルーチン (Goroutine): Go言語の軽量な並行処理の単位です。
go
キーワードを使って関数呼び出しの前に置くことで、その関数を新しいゴルーチンで実行します。ゴルーチンはOSのスレッドよりもはるかに軽量で、数千、数万のゴルーチンを同時に実行することが可能です。 - 競合状態 (Race Condition): 複数の並行プロセス(この場合はゴルーチンとメインのテスト関数)が共有リソース(この場合はファイルディスクリプタ
f
)にアクセスし、そのアクセス順序によって結果が非決定的に変わる状態を指します。今回のケースでは、ファイルf
のクローズと、そのファイルから読み取るゴルーチンの処理完了のタイミングが競合していました。 SIGPIPE
シグナル: UNIX系オペレーティングシステムで発生するシグナルの一つです。パイプの読み込み側が閉じられた後に、書き込み側がそのパイプにデータを書き込もうとした場合に、書き込み側のプロセスに送信されます。デフォルトではプロセスを終了させます。EBADF
(Bad File Descriptor): 無効なファイルディスクリプタに対して操作を行おうとした際に返されるエラーコードです。ファイルが既に閉じられている、または存在しないファイルディスクリプタを参照している場合に発生します。
技術的詳細
元の TestNonSeekable
テストは、os.Pipe()
を利用して、tar.NewReader
に非シーク可能なストリームを提供していました。これは、f
というファイルからデータを読み込み、それを w
(パイプの書き込み側) に書き込むゴルーチンを起動し、tar.NewReader
には r
(パイプの読み込み側) を渡すという構造でした。
// 変更前のコードの抜粋
r, w, err := os.Pipe()
// ...
go func() {
rdbuf := make([]uint8, 1<<16)
for {
nr, err := f.Read(rdbuf) // f から読み込み
w.Write(rdbuf[0:nr]) // w に書き込み
if err == io.EOF {
break
}
}
w.Close() // ゴルーチン内でパイプの書き込み側を閉じる
}()
tr := NewReader(r) // tar.NewReader にパイプの読み込み側を渡す
// ...
defer f.Close() // テスト関数が終了する際に f を閉じる
この設計における競合状態の核心は、defer f.Close()
とゴルーチン内の w.Close()
の実行タイミングにありました。
f.Close()
の早期実行:defer f.Close()
はTestNonSeekable
関数がリターンする直前に実行されます。しかし、ゴルーチンがf
からio.EOF
を受け取り、w.Close()
を呼び出す処理は非同期で行われます。もしTestNonSeekable
関数がゴルーチンがio.EOF
を完全に処理し終える前にリターンし始めると(例えば、tr.Next()
がエラーを返してループを抜けた場合など)、f.Close()
が先に実行されてしまう可能性がありました。EBADF
エラー:f
が閉じられた後も、ゴルーチンがf.Read()
を呼び出そうとすると、無効なファイルディスクリプタに対して操作を行おうとするため、EBADF
エラーが発生します。SIGPIPE
シグナル: さらに、tar.NewReader
がパイプr
から読み込みを終えてパイプの読み込み側が閉じられた後も、ゴルーチンがw.Write()
を呼び出し続けると、OSはSIGPIPE
シグナルを送信します。これは、パイプの読み込み側が既に閉じられているにもかかわらず、書き込み側が書き込みを継続しようとした場合に発生します。このシグナルが捕捉されない場合、テストプロセスは異常終了します。
この問題を解決するため、コミットでは os.Pipe()
と中間ゴルーチンを完全に排除し、より直接的なアプローチを採用しました。
// 変更後のコードの抜粋
type readerOnly struct {
io.Reader
}
tr := NewReader(readerOnly{f}) // tar.NewReader に直接 f をラップしたものを渡す
新しいアプローチでは、readerOnly
というシンプルな構造体を定義し、これに元のファイル f
を io.Reader
インターフェースとして埋め込みます。readerOnly
は io.Reader
インターフェースを実装しているため、readerOnly
型のインスタンスは、埋め込まれた io.Reader
のすべてのメソッド(この場合は Read
メソッド)を自動的に継承します。これにより、readerOnly
型自体が io.Reader
インターフェースを実装しているとみなされます。
この変更により、以下の利点が得られます。
- 競合状態の解消:
os.Pipe()
とそれに関連するゴルーチンが不要になるため、f.Close()
とパイプへの書き込みのタイミングに関する競合状態がなくなります。tar.NewReader
は直接f
から読み込むため、f
のクローズはtar.NewReader
がio.EOF
を受け取った後に安全に行われます。 - コードの簡素化: パイプの設定とゴルーチンの管理が不要になり、テストコードが大幅に簡素化されます。
SIGPIPE
の回避: パイプへの書き込みがなくなるため、SIGPIPE
シグナルが発生する可能性がなくなります。
この修正は、テストの信頼性を高め、特定のOS環境での不安定な失敗を解消する上で非常に効果的です。
コアとなるコードの変更箇所
--- a/src/pkg/archive/tar/reader_test.go
+++ b/src/pkg/archive/tar/reader_test.go
@@ -240,31 +240,20 @@ func TestNonSeekable(t *testing.T) {
}\n \tdefer f.Close()\n \n-\t// pipe the data in\n-\tr, w, err := os.Pipe()\n-\tif err != nil {\n-\t\tt.Fatalf(\"Unexpected error %s\", err)\n+\ttype readerOnly struct {\n+\t\tio.Reader\n \t}\n-\tgo func() {\n-\t\trdbuf := make([]uint8, 1<<16)\n-\t\tfor {\n-\t\t\tnr, err := f.Read(rdbuf)\n-\t\t\tw.Write(rdbuf[0:nr])\n-\t\t\tif err == io.EOF {\n-\t\t\t\tbreak\n-\t\t\t}\n-\t\t}\n-\t\tw.Close()\n-\t}()\n-\n-\ttr := NewReader(r)\n+\ttr := NewReader(readerOnly{f})\n \tnread := 0\n \n \tfor ; ; nread++ {\n-\t\thdr, err := tr.Next()\n-\t\tif hdr == nil || err == io.EOF {\n+\t\t_, err := tr.Next()\n+\t\tif err == io.EOF {\n \t\t\tbreak\n \t\t}\n+\t\tif err != nil {\n+\t\t\tt.Fatalf(\"Unexpected error: %v\", err)\n+\t\t}\n \t}\n \n \tif nread != len(test.headers) {\n```
## コアとなるコードの解説
変更の核心は、`os.Pipe()` を使用した間接的なデータ転送メカニズムを削除し、代わりに `io.Reader` インターフェースを直接利用する点にあります。
1. **`os.Pipe()` とゴルーチンの削除**:
変更前は、`os.Pipe()` で作成したパイプの書き込み側 `w` にファイル `f` から読み込んだデータをゴルーチンで書き込み、パイプの読み込み側 `r` を `tar.NewReader` に渡していました。この複雑な設定が競合状態の原因でした。
変更後、この部分のコード(`r, w, err := os.Pipe()` から `w.Close()` までのゴルーチン全体)が削除されました。
2. **`readerOnly` 構造体の導入**:
新しく `readerOnly` という構造体が定義されました。
```go
type readerOnly struct {
io.Reader
}
```
この構造体は、`io.Reader` インターフェースを匿名フィールドとして埋め込んでいます。Go言語の埋め込み(embedding)の特性により、`readerOnly` 型のインスタンスは、埋め込まれた `io.Reader` のすべてのメソッド(この場合は `Read` メソッド)を自動的に継承します。これにより、`readerOnly` 型自体が `io.Reader` インターフェースを実装しているとみなされます。
3. **`tar.NewReader` への直接的な `io.Reader` の提供**:
変更前は `tr := NewReader(r)` と、パイプの読み込み側 `r` を `tar.NewReader` に渡していました。
変更後、`tr := NewReader(readerOnly{f})` となりました。これは、元のファイル `f`(これも `io.Reader` インターフェースを実装しています)を `readerOnly` 構造体でラップし、その `readerOnly` インスタンスを直接 `tar.NewReader` に渡しています。
`tar.NewReader` は `io.Reader` を引数として受け取るため、`readerOnly{f}` は有効な引数となります。これにより、`tar.NewReader` は直接 `f` からデータを読み込むようになります。
4. **エラーハンドリングの追加**:
`tr.Next()` の呼び出し後、以前は `if hdr == nil || err == io.EOF` という条件でループを抜けていましたが、`hdr == nil` のチェックが削除され、`if err == io.EOF` のみでループを抜けるようになりました。
さらに、`if err != nil` のチェックが追加され、`io.EOF` 以外のエラーが発生した場合には `t.Fatalf` でテストを失敗させるようになりました。これにより、より堅牢なエラーハンドリングが実現されています。
この変更により、データフローが大幅に簡素化され、ファイルクローズとデータ読み込みの間の競合状態が根本的に解消されました。`tar.NewReader` は直接 `f` から読み込むため、`f` が閉じられるタイミングは `tar.NewReader` が `io.EOF` を受け取った後となり、安全性が確保されます。
## 関連リンク
* Go CL (Change List) 5554083: [https://golang.org/cl/5554083](https://golang.org/cl/5554083) (このコミットに対応するGoのコードレビューシステム上の変更リスト)
## 参考にした情報源リンク
* Go言語の `io` パッケージに関するドキュメント: [https://pkg.go.dev/io](https://pkg.go.dev/io)
* Go言語の `os` パッケージに関するドキュメント: [https://pkg.go.dev/os](https://pkg.go.dev/os)
* Go言語の並行処理(ゴルーチンとチャネル)に関する一般的な情報源
* UNIX系システムにおける `SIGPIPE` シグナルに関する情報源
* Go言語の `defer` ステートメントに関する情報源
* Go言語の埋め込み(embedding)に関する情報源