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

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

このコミットは、Go言語の標準ライブラリioパッケージ内のパイプ(Pipe)の実装に関するバグ修正です。具体的には、io.PipeWriterCloseされた後に書き込みが行われた場合でも、エラーを返さずに書き込みが成功してしまうという問題に対処しています。この修正により、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.PipeWriterCloseメソッドが呼び出された後でも、そのPipeWriterに対してWrite操作を行うことが可能でした。これは、閉じられたパイプへの書き込みが成功してしまうという予期せぬ動作を引き起こし、プログラムのロジックに混乱をもたらす可能性がありました。

通常、リソースが閉じられた後には、それに対する操作はエラーを返すことが期待されます。このバグは、PipeWriterが閉じられた状態を適切に伝達せず、後続の書き込みが成功したかのように振る舞うことで、開発者が誤った仮定に基づいてコードを記述してしまうリスクがありました。この修正は、このような不整合を解消し、PipeWriterが閉じられた後には明確にErrClosedPipeを返すことで、より堅牢で予測可能なI/O操作を保証することを目的としています。

前提知識の解説

このコミットを理解するためには、以下のGo言語のioパッケージに関する基本的な概念を理解しておく必要があります。

  • io.Pipe(): io.Pipe()関数は、メモリ内で接続されたio.PipeReaderio.PipeWriterのペアを作成します。これは、あるゴルーチンがデータを書き込み、別のゴルーチンがそのデータを読み取るための同期的なパイプラインを構築する際に非常に便利です。os.Pipeとは異なり、ファイルディスクリプタを使用せず、純粋にGoのゴルーチンとチャネルによって実装されます。

  • io.PipeWriter: io.PipeWriterは、io.Writerインターフェースを実装しており、パイプの書き込み側を表します。Writeメソッドを通じてデータをパイプに書き込むことができます。

  • io.PipeReader: io.PipeReaderは、io.Readerインターフェースを実装しており、パイプの読み込み側を表します。Readメソッドを通じてパイプからデータを読み取ることができます。

  • Close() メソッド: io.PipeWriterio.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メソッドを終了させます。これにより、PipeWriterCloseされた後にWriteが呼び出された場合、p.werrにはErrClosedPipeが設定されているため、このチェックによって適切なエラーが呼び出し元に返されるようになります。

また、この変更を検証するために、pipe_test.goTestWriteAfterWriterCloseという新しいテストケースが追加されました。このテストは、PipeWriterを閉じた後に書き込みを試み、その際にErrClosedPipeが返されることを確認します。これにより、修正が正しく機能していることが保証されます。

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

変更は主にsrc/pkg/io/pipe.gosrc/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.PipeWriterWriteメソッドから内部的に呼び出される関数です。このメソッドは、書き込まれるデータを処理し、読み込み側が利用できるようにします。

追加された以下の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.werrnilでなければ(つまり、パイプが閉じられているか、以前にエラーが発生している場合)、writeメソッドの戻り値であるerrio.ErrClosedPipeを設定します。
  • return: そして、直ちにメソッドを終了します。これにより、閉じられたパイプへの書き込み操作は、データが実際に書き込まれることなく、期待されるエラーを返して終了します。

この変更により、PipeWriterが閉じられた後にWriteが呼び出された場合、p.werrに設定されたErrClosedPipeが検出され、そのエラーが呼び出し元に適切に伝播されるようになります。

src/pkg/io/pipe_test.go の変更

TestWriteAfterWriterCloseという新しいテスト関数が追加されました。このテストは、修正が正しく機能していることを検証するためのものです。

  1. r, w := Pipe(): 新しいパイプを作成します。
  2. ゴルーチン内で最初の書き込み (w.Write([]byte("hello"))) を行い、その後すぐにw.Close()を呼び出してPipeWriterを閉じます。
  3. 閉じられたPipeWriterに対して、2回目の書き込み (w.Write([]byte("world"))) を試みます。この書き込みの結果として返されるエラーをwriteErrに格納します。
  4. メインゴルーチンでは、ReadFull(r, buf)で最初の書き込みデータ ("hello") を読み取ります。
  5. ゴルーチンが終了するのを待ちます (<-done)。
  6. 最後に、読み取ったデータが "hello" であることと、2回目の書き込みで返されたエラー (writeErr) がErrClosedPipeであることをアサートします。

このテストは、PipeWriterが閉じられた後に書き込みを試みると、期待通りErrClosedPipeが返されることを明確に示しており、コミットの修正が意図した通りに動作していることを保証します。

関連リンク

参考にした情報源リンク