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

[インデックス 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() の実行タイミングにありました。

  1. f.Close() の早期実行: defer f.Close()TestNonSeekable 関数がリターンする直前に実行されます。しかし、ゴルーチンが f から io.EOF を受け取り、w.Close() を呼び出す処理は非同期で行われます。もし TestNonSeekable 関数がゴルーチンが io.EOF を完全に処理し終える前にリターンし始めると(例えば、tr.Next() がエラーを返してループを抜けた場合など)、f.Close() が先に実行されてしまう可能性がありました。
  2. EBADF エラー: f が閉じられた後も、ゴルーチンが f.Read() を呼び出そうとすると、無効なファイルディスクリプタに対して操作を行おうとするため、EBADF エラーが発生します。
  3. SIGPIPE シグナル: さらに、tar.NewReader がパイプ r から読み込みを終えてパイプの読み込み側が閉じられた後も、ゴルーチンが w.Write() を呼び出し続けると、OSは SIGPIPE シグナルを送信します。これは、パイプの読み込み側が既に閉じられているにもかかわらず、書き込み側が書き込みを継続しようとした場合に発生します。このシグナルが捕捉されない場合、テストプロセスは異常終了します。

この問題を解決するため、コミットでは os.Pipe() と中間ゴルーチンを完全に排除し、より直接的なアプローチを採用しました。

// 変更後のコードの抜粋
type readerOnly struct {
    io.Reader
}
tr := NewReader(readerOnly{f}) // tar.NewReader に直接 f をラップしたものを渡す

新しいアプローチでは、readerOnly というシンプルな構造体を定義し、これに元のファイル fio.Reader インターフェースとして埋め込みます。readerOnlyio.Reader インターフェースを実装しているため、tar.NewReader はこれを直接受け入れることができます。

この変更により、以下の利点が得られます。

  • 競合状態の解消: os.Pipe() とそれに関連するゴルーチンが不要になるため、f.Close() とパイプへの書き込みのタイミングに関する競合状態がなくなります。tar.NewReader は直接 f から読み込むため、f のクローズは tar.NewReaderio.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 が閉じられてしまう可能性がありました。

具体的には、以下の競合状態が発生していました。

  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.Readerio.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() の実行タイミングにありました。

  1. f.Close() の早期実行: defer f.Close()TestNonSeekable 関数がリターンする直前に実行されます。しかし、ゴルーチンが f から io.EOF を受け取り、w.Close() を呼び出す処理は非同期で行われます。もし TestNonSeekable 関数がゴルーチンが io.EOF を完全に処理し終える前にリターンし始めると(例えば、tr.Next() がエラーを返してループを抜けた場合など)、f.Close() が先に実行されてしまう可能性がありました。
  2. EBADF エラー: f が閉じられた後も、ゴルーチンが f.Read() を呼び出そうとすると、無効なファイルディスクリプタに対して操作を行おうとするため、EBADF エラーが発生します。
  3. SIGPIPE シグナル: さらに、tar.NewReader がパイプ r から読み込みを終えてパイプの読み込み側が閉じられた後も、ゴルーチンが w.Write() を呼び出し続けると、OSは SIGPIPE シグナルを送信します。これは、パイプの読み込み側が既に閉じられているにもかかわらず、書き込み側が書き込みを継続しようとした場合に発生します。このシグナルが捕捉されない場合、テストプロセスは異常終了します。

この問題を解決するため、コミットでは os.Pipe() と中間ゴルーチンを完全に排除し、より直接的なアプローチを採用しました。

// 変更後のコードの抜粋
type readerOnly struct {
    io.Reader
}
tr := NewReader(readerOnly{f}) // tar.NewReader に直接 f をラップしたものを渡す

新しいアプローチでは、readerOnly というシンプルな構造体を定義し、これに元のファイル fio.Reader インターフェースとして埋め込みます。readerOnlyio.Reader インターフェースを実装しているため、readerOnly 型のインスタンスは、埋め込まれた io.Reader のすべてのメソッド(この場合は Read メソッド)を自動的に継承します。これにより、readerOnly 型自体が io.Reader インターフェースを実装しているとみなされます。

この変更により、以下の利点が得られます。

  • 競合状態の解消: os.Pipe() とそれに関連するゴルーチンが不要になるため、f.Close() とパイプへの書き込みのタイミングに関する競合状態がなくなります。tar.NewReader は直接 f から読み込むため、f のクローズは tar.NewReaderio.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)に関する情報源