[インデックス 17196] ファイルの概要
このコミットは、Go言語の標準ライブラリio
パッケージ内のパイプ(Pipe
)の実装に関するバグ修正です。具体的には、io.PipeWriter
がClose
された後に書き込みが行われた場合でも、エラーを返さずに書き込みが成功してしまうという問題に対処しています。この修正により、PipeWriter
が閉じられた後にはErrClosedPipe
が返されるようになり、予期せぬ動作を防ぎます。
コミット
commit 4be93851c37281af977f1c1aaa2e2c65c8f40ce0
Author: Rick Arnold <rickarnoldjr@gmail.com>
Date: Tue Aug 13 11:04:09 2013 -0700
io: prevent write to PipeWriter after Close
Return an ErrClosedPipe rather than allowing the write to proceed.
Fixes #5330.
R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/12541053
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/4be93851c37281af977f1c1aaa2e2c65c8f40ce0
元コミット内容
io: prevent write to PipeWriter after Close
Return an ErrClosedPipe rather than allowing the write to proceed.
Fixes #5330.
R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/12541053
変更の背景
この変更は、Go言語のio
パッケージにおけるPipe
の実装に関するバグ(Issue 5330)を修正するために行われました。元の実装では、io.PipeWriter
のClose
メソッドが呼び出された後でも、そのPipeWriter
に対してWrite
操作を行うことが可能でした。これは、閉じられたパイプへの書き込みが成功してしまうという予期せぬ動作を引き起こし、プログラムのロジックに混乱をもたらす可能性がありました。
通常、リソースが閉じられた後には、それに対する操作はエラーを返すことが期待されます。このバグは、PipeWriter
が閉じられた状態を適切に伝達せず、後続の書き込みが成功したかのように振る舞うことで、開発者が誤った仮定に基づいてコードを記述してしまうリスクがありました。この修正は、このような不整合を解消し、PipeWriter
が閉じられた後には明確にErrClosedPipe
を返すことで、より堅牢で予測可能なI/O操作を保証することを目的としています。
前提知識の解説
このコミットを理解するためには、以下のGo言語のio
パッケージに関する基本的な概念を理解しておく必要があります。
-
io.Pipe()
:io.Pipe()
関数は、メモリ内で接続されたio.PipeReader
とio.PipeWriter
のペアを作成します。これは、あるゴルーチンがデータを書き込み、別のゴルーチンがそのデータを読み取るための同期的なパイプラインを構築する際に非常に便利です。os.Pipe
とは異なり、ファイルディスクリプタを使用せず、純粋にGoのゴルーチンとチャネルによって実装されます。 -
io.PipeWriter
:io.PipeWriter
は、io.Writer
インターフェースを実装しており、パイプの書き込み側を表します。Write
メソッドを通じてデータをパイプに書き込むことができます。 -
io.PipeReader
:io.PipeReader
は、io.Reader
インターフェースを実装しており、パイプの読み込み側を表します。Read
メソッドを通じてパイプからデータを読み取ることができます。 -
Close()
メソッド:io.PipeWriter
とio.PipeReader
の両方にはClose()
メソッドがあります。PipeWriter.Close()
: 書き込み側を閉じます。これにより、読み込み側はそれ以上データが来ないことを知り、残りのデータを読み終えた後にio.EOF
エラーを受け取ります。PipeReader.Close()
: 読み込み側を閉じます。これにより、書き込み側はio.ErrClosedPipe
エラーを受け取ります。
-
io.ErrClosedPipe
:io.ErrClosedPipe
は、パイプが閉じられた後に操作(特に書き込み)が行われた場合に返されるエラーです。このエラーは、パイプがもはや有効な状態ではないことを示し、それ以上の操作が意味をなさないことを通知します。
このコミットの核心は、PipeWriter.Close()
が呼び出された後にPipeWriter.Write()
が呼び出された際に、ErrClosedPipe
が適切に返されるようにすることです。
技術的詳細
このコミットの技術的詳細は、io
パッケージ内のpipe.go
ファイルにおけるpipe
構造体のwrite
メソッドの変更に集約されます。
io.Pipe
は内部的にpipe
という構造体を使用しており、この構造体は読み書きの状態を管理するためのフィールドを持っています。特に重要なのは、書き込みエラーを保持するp.werr
フィールドです。
変更前のwrite
メソッドでは、PipeWriter.Close()
が呼び出された際にp.werr
が設定されても、write
メソッドの冒頭でこのエラーをチェックするロジックがありませんでした。そのため、PipeWriter
が閉じられた後でも、write
メソッドはp.werr
の状態を無視して書き込み処理を続行しようとしていました。結果として、閉じられたパイプへの書き込みがエラーを返さずに成功したかのように見えてしまうという問題が発生していました。
このコミットでは、write
メソッドの冒頭に以下のチェックを追加することでこの問題を解決しています。
if p.werr != nil {
err = ErrClosedPipe
return
}
このコードは、pipe
構造体のp.werr
フィールドがnil
でない(つまり、以前に書き込みエラーが発生しているか、パイプが閉じられている)場合に、直ちにErrClosedPipe
を返してwrite
メソッドを終了させます。これにより、PipeWriter
がClose
された後にWrite
が呼び出された場合、p.werr
にはErrClosedPipe
が設定されているため、このチェックによって適切なエラーが呼び出し元に返されるようになります。
また、この変更を検証するために、pipe_test.go
にTestWriteAfterWriterClose
という新しいテストケースが追加されました。このテストは、PipeWriter
を閉じた後に書き込みを試み、その際にErrClosedPipe
が返されることを確認します。これにより、修正が正しく機能していることが保証されます。
コアとなるコードの変更箇所
変更は主にsrc/pkg/io/pipe.go
とsrc/pkg/io/pipe_test.go
の2つのファイルで行われています。
src/pkg/io/pipe.go
--- a/src/pkg/io/pipe.go
+++ b/src/pkg/io/pipe.go
@@ -74,6 +74,10 @@ func (p *pipe) write(b []byte) (n int, err error) {
p.l.Lock()
defer p.l.Unlock()
+ if p.werr != nil {
+ err = ErrClosedPipe
+ return
+ }
p.data = b
p.rwait.Signal()
for {
src/pkg/io/pipe_test.go
--- a/src/pkg/io/pipe_test.go
+++ b/src/pkg/io/pipe_test.go
@@ -268,3 +268,35 @@ func TestWriteNil(t *testing.T) {
ReadFull(r, b[0:2])
r.Close()
}
+
+func TestWriteAfterWriterClose(t *testing.T) {
+ r, w := Pipe()
+
+ done := make(chan bool)
+ var writeErr error
+ go func() {
+ _, err := w.Write([]byte("hello"))
+ if err != nil {
+ t.Errorf("got error: %q; expected none", err)
+ }
+ w.Close()
+ _, writeErr = w.Write([]byte("world"))
+ done <- true
+ }()
+
+ buf := make([]byte, 100)
+ var result string
+ n, err := ReadFull(r, buf)
+ if err != nil && err != ErrUnexpectedEOF {
+ t.Fatalf("got: %q; want: %q", err, ErrUnexpectedEOF)
+ }
+ result = string(buf[0:n])
+ <-done
+
+ if result != "hello" {
+ t.Errorf("got: %q; want: %q", result, "hello")
+ }
+ if writeErr != ErrClosedPipe {
+ t.Errorf("got: %q; want: %q", writeErr, ErrClosedPipe)
+ }
+}
コアとなるコードの解説
src/pkg/io/pipe.go
の変更
pipe
構造体のwrite
メソッドは、io.PipeWriter
のWrite
メソッドから内部的に呼び出される関数です。このメソッドは、書き込まれるデータを処理し、読み込み側が利用できるようにします。
追加された以下の4行がこのコミットの核心です。
if p.werr != nil {
err = ErrClosedPipe
return
}
p.l.Lock()
とdefer p.l.Unlock()
の間、つまりミューテックスで保護された領域の冒頭に挿入されています。これにより、p.werr
へのアクセスがスレッドセーフに行われます。if p.werr != nil
: これは、pipe
構造体のwerr
フィールドがnil
でないかどうかをチェックします。werr
フィールドは、PipeWriter
が閉じられた際や、書き込み中にエラーが発生した際に、そのエラーを保持するために使用されます。PipeWriter.Close()
が呼び出されると、このwerr
フィールドにErrClosedPipe
が設定されます。err = ErrClosedPipe
: もしp.werr
がnil
でなければ(つまり、パイプが閉じられているか、以前にエラーが発生している場合)、write
メソッドの戻り値であるerr
にio.ErrClosedPipe
を設定します。return
: そして、直ちにメソッドを終了します。これにより、閉じられたパイプへの書き込み操作は、データが実際に書き込まれることなく、期待されるエラーを返して終了します。
この変更により、PipeWriter
が閉じられた後にWrite
が呼び出された場合、p.werr
に設定されたErrClosedPipe
が検出され、そのエラーが呼び出し元に適切に伝播されるようになります。
src/pkg/io/pipe_test.go
の変更
TestWriteAfterWriterClose
という新しいテスト関数が追加されました。このテストは、修正が正しく機能していることを検証するためのものです。
r, w := Pipe()
: 新しいパイプを作成します。- ゴルーチン内で最初の書き込み (
w.Write([]byte("hello"))
) を行い、その後すぐにw.Close()
を呼び出してPipeWriter
を閉じます。 - 閉じられた
PipeWriter
に対して、2回目の書き込み (w.Write([]byte("world"))
) を試みます。この書き込みの結果として返されるエラーをwriteErr
に格納します。 - メインゴルーチンでは、
ReadFull(r, buf)
で最初の書き込みデータ ("hello"
) を読み取ります。 - ゴルーチンが終了するのを待ちます (
<-done
)。 - 最後に、読み取ったデータが
"hello"
であることと、2回目の書き込みで返されたエラー (writeErr
) がErrClosedPipe
であることをアサートします。
このテストは、PipeWriter
が閉じられた後に書き込みを試みると、期待通りErrClosedPipe
が返されることを明確に示しており、コミットの修正が意図した通りに動作していることを保証します。
関連リンク
- Go Issue 5330: https://github.com/golang/go/issues/5330
- Gerrit Change-Id: https://golang.org/cl/12541053