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

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

このコミットは、Go言語の標準ライブラリioパッケージ内のMultiReaderおよびMultiWriter関数における重要な修正を導入しています。具体的には、これらの関数に渡されるReaderまたはWriterのスライスが、関数内部でコピーされるように変更されました。これにより、元のスライスが後から変更されても、MultiReaderMultiWriterの動作に予期せぬ影響が出ないようになります。

コミット

commit 211618c26ebe5fe931d7366b94e15fbd92584555
Author: Russ Cox <rsc@golang.org>
Date:   Mon May 12 23:38:35 2014 -0400

    io: copy slice argument in MultiReader and MultiWriter
    
    Replaces CL 91240045.
    Fixes #7809.
    
    LGTM=bradfitz
    R=golang-codereviews, minux.ma
    CC=adg, bradfitz, golang-codereviews, iant, r
    https://golang.org/cl/94380043

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

https://github.com/golang/go/commit/211618c26ebe5fe931d7366b94e15fbd92584555

元コミット内容

このコミットの元のメッセージは以下の通りです。

io: copy slice argument in MultiReader and MultiWriter

Replaces CL 91240045.
Fixes #7809.

LGTM=bradfitz
R=golang-codereviews, minux.ma
CC=adg, bradfitz, golang-codereviews, iant, r
https://golang.org/cl/94380043

このメッセージは、MultiReaderMultiWriterが引数として受け取るスライスをコピーするように変更されたことを簡潔に示しています。また、関連する変更リスト(CL)や修正されたIssue番号(#7809)も記載されています。

変更の背景

この変更の背景には、io.MultiReaderおよびio.MultiWriterの以前の実装における潜在的なバグがありました。これらの関数は可変長引数(...Reader...Writer)を受け取り、内部でそれらをスライスとして扱います。しかし、以前の実装では、このスライスが内部で直接参照されており、コピーが作成されていませんでした。

この「参照渡し」の挙動は、以下のような問題を引き起こす可能性がありました。

  1. 予期せぬ動作の変更: MultiReaderMultiWriterが作成された後で、それらに渡された元のスライスが外部から変更された場合、MultiReaderMultiWriterの内部状態もその変更を反映してしまい、予期せぬ読み書きの挙動を示す可能性がありました。例えば、スライス内のReaderWriternilに設定されたり、別のインスタンスに置き換えられたりすると、実行時エラーやデータの破損につながる恐れがありました。
  2. デバッグの困難さ: このような問題は、コードの異なる部分でスライスが共有され、非同期的に変更される場合に特にデバッグが困難になります。

このコミットは、Issue #7809で報告された問題を解決するために導入されました。この問題は、MultiReaderが作成された後に、その基になるスライスが変更されると、MultiReaderが正しく動作しないというものでした。

前提知識の解説

このコミットを理解するためには、以下のGo言語の概念とioパッケージの基本的な知識が必要です。

  1. スライス (Slice): Go言語のスライスは、配列のセグメントを参照するデータ構造です。スライス自体は、基になる配列へのポインタ、長さ(len)、容量(cap)の3つの要素で構成されます。スライスを関数に渡す際、スライスヘッダ(ポインタ、長さ、容量)は値渡しされますが、そのポインタが指す基になる配列のデータは共有されます。したがって、関数内でスライスの要素を変更すると、元のスライスの基になる配列のデータも変更されます。 この特性が、本コミットで修正される問題の根本原因でした。

  2. 可変長引数 (Variadic Functions): Go言語では、関数の最後の引数に...を付けることで、その関数が0個以上の引数を受け取れるように定義できます。例えば、func MultiReader(readers ...Reader) Readerのように定義された場合、readersは関数内で[]Reader型のスライスとして扱われます。

  3. io.Reader インターフェース: io.Readerは、Go言語でデータを読み込むための基本的なインターフェースです。

    type Reader interface {
        Read(p []byte) (n int, err error)
    }
    

    Readメソッドは、バイトスライスpにデータを読み込み、読み込んだバイト数nとエラーerrを返します。

  4. io.Writer インターフェース: io.Writerは、Go言語でデータを書き込むための基本的なインターフェースです。

    type Writer interface {
        Write(p []byte) (n int, err error)
    }
    

    Writeメソッドは、バイトスライスpからデータを書き込み、書き込んだバイト数nとエラーerrを返します。

  5. io.MultiReader 関数: 複数のio.Readerを結合し、それらをあたかも単一のio.Readerであるかのように扱うための関数です。MultiReaderからReadを呼び出すと、内部的に最初のReaderから読み込みを試み、それがEOFに達すると次のReaderから読み込みを続けます。

  6. io.MultiWriter 関数: 複数のio.Writerを結合し、それらすべてに同じデータを書き込むための関数です。MultiWriterWriteを呼び出すと、内部的にすべての登録されたWriterに対して同じデータを書き込みます。これはUnixのteeコマンドに似ています。

  7. copy 関数: Go言語の組み込み関数copy(dst, src []Type)は、srcスライスからdstスライスに要素をコピーします。コピーされる要素数は、len(dst)len(src)の小さい方になります。この関数は、スライスの内容を別のメモリ領域に複製するために使用されます。

技術的詳細

このコミットの技術的な核心は、MultiReaderMultiWriterのコンストラクタ関数内で、入力として受け取ったReaderまたはWriterのスライスを明示的にコピーする点にあります。

以前の実装では、以下のようになっていました。

// 変更前 (概念的なコード)
func MultiReader(readers ...Reader) Reader {
    return &multiReader{readers} // readersスライスが直接参照される
}

func MultiWriter(writers ...Writer) Writer {
    return &multiWriter{writers} // writersスライスが直接参照される
}

このコードでは、multiReader構造体やmultiWriter構造体の内部フィールドreaderswritersが、引数として渡されたスライスreaderswriters同じ基になる配列を共有していました。

このコミットによって、以下のように変更されました。

// 変更後
func MultiReader(readers ...Reader) Reader {
    r := make([]Reader, len(readers)) // 新しいスライスを作成
    copy(r, readers)                  // 元のスライスの内容を新しいスライスにコピー
    return &multiReader{r}            // コピーされたスライスを参照
}

func MultiWriter(writers ...Writer) Writer {
    w := make([]Writer, len(writers)) // 新しいスライスを作成
    copy(w, writers)                  // 元のスライスの内容を新しいスライスにコピー
    return &multiWriter{w}            // コピーされたスライスを参照
}

この変更により、MultiReaderMultiWriterが内部で保持するスライスは、コンストラクタが呼び出された時点での入力スライスの独立したコピーになります。したがって、コンストラクタ呼び出し後に元のスライスが外部で変更されても、MultiReaderMultiWriterの内部状態には影響が及ばなくなり、より堅牢で予測可能な動作が保証されます。

この修正は、Go言語の「値渡し」と「参照渡し」のセマンティクス、特にスライスがどのように扱われるかについての理解を深める上で重要です。スライスヘッダは値渡しされますが、そのヘッダが指す基になる配列は共有されるため、意図しない副作用を避けるためには、必要に応じて明示的なコピーを行う必要があります。このコミットは、まさにその原則をioパッケージの重要な関数に適用したものです。

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

変更はsrc/pkg/io/multi.gosrc/pkg/io/multi_test.goの2つのファイルにわたります。

src/pkg/io/multi.go

--- a/src/pkg/io/multi.go
+++ b/src/pkg/io/multi.go
@@ -29,7 +29,9 @@ func (mr *multiReader) Read(p []byte) (n int, err error) {
 // inputs have returned EOF, Read will return EOF.  If any of the readers
 // return a non-nil, non-EOF error, Read will return that error.
 func MultiReader(readers ...Reader) Reader {
-	return &multiReader{readers}
+	r := make([]Reader, len(readers))
+	copy(r, readers)
+	return &multiReader{r}
 }
 
 type multiWriter struct {
@@ -53,5 +55,7 @@ func (t *multiWriter) Write(p []byte) (n int, err error) {
 // MultiWriter creates a writer that duplicates its writes to all the
 // provided writers, similar to the Unix tee(1) command.
 func MultiWriter(writers ...Writer) Writer {
-	return &multiWriter{writers}
+	w := make([]Writer, len(writers))
+	copy(w, writers)
+	return &multiWriter{w}
 }

src/pkg/io/multi_test.go

--- a/src/pkg/io/multi_test.go
+++ b/src/pkg/io/multi_test.go
@@ -9,6 +9,7 @@ import (
 	"crypto/sha1"
 	"fmt"
 	. "io"
+	"io/ioutil"
 	"strings"
 	"testing"
 )
@@ -86,3 +87,29 @@ func TestMultiWriter(t *testing.T) {
 		t.Errorf("expected %q; got %q", sourceString, sink.String())
 	}
 }
+
+// Test that MultiReader copies the input slice and is insulated from future modification.
+func TestMultiReaderCopy(t *testing.T) {
+	slice := []Reader{strings.NewReader("hello world")}
+	r := MultiReader(slice...)
+	slice[0] = nil
+	data, err := ioutil.ReadAll(r)
+	if err != nil || string(data) != "hello world" {
+		t.Errorf("ReadAll() = %q, %v, want %q, nil", data, err, "hello world")
+	}
+}
+
+// Test that MultiWriter copies the input slice and is insulated from future modification.
+func TestMultiWriterCopy(t *testing.T) {
+	var buf bytes.Buffer
+	slice := []Writer{&buf}
+	w := MultiWriter(slice...)
+	slice[0] = nil
+	n, err := w.Write([]byte("hello world"))
+	if err != nil || n != 11 {
+		t.Errorf("Write(`hello world`) = %d, %v, want 11, nil", n, err)
+	}
+	if buf.String() != "hello world" {
+		t.Errorf("buf.String() = %q, want %q", buf.String(), "hello world")
+	}
+}

コアとなるコードの解説

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

  • MultiReader 関数:

    • 変更前: return &multiReader{readers}
      • これは、引数として受け取ったreadersスライスを、multiReader構造体の内部フィールドreadersに直接割り当てていました。これにより、multiReaderの内部スライスと外部のreadersスライスが同じ基になる配列を共有していました。
    • 変更後:
      r := make([]Reader, len(readers))
      copy(r, readers)
      return &multiReader{r}
      
      • まず、make([]Reader, len(readers))によって、元のreadersスライスと同じ長さの新しいReaderスライスrが作成されます。この新しいスライスは、独自の基になる配列を持っています。
      • 次に、copy(r, readers)によって、元のreadersスライスの内容(つまり、各Readerインターフェースの値)が、新しく作成されたスライスrにコピーされます。
      • 最後に、&multiReader{r}として、コピーされたスライスrmultiReader構造体に渡されます。これにより、multiReaderは外部のスライスとは独立した自身のReaderスライスのコピーを持つことになります。
  • MultiWriter 関数:

    • MultiReaderと同様に、MultiWriter関数も同様のロジックで変更されました。
    • 変更前: return &multiWriter{writers}
    • 変更後:
      w := make([]Writer, len(writers))
      copy(w, writers)
      return &multiWriter{w}
      
      • これにより、MultiWriterも外部のWriterスライスから独立したコピーを持つようになります。

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

このコミットでは、上記の変更が正しく機能することを検証するための新しいテストケースが追加されています。

  • TestMultiReaderCopy:

    • このテストは、MultiReaderが入力スライスをコピーし、その後の外部からの変更に対して影響を受けないことを確認します。
    • slice := []Reader{strings.NewReader("hello world")}: strings.NewReaderで作成された単一のReaderを含むスライスを定義します。
    • r := MultiReader(slice...): このスライスからMultiReaderを作成します。
    • slice[0] = nil: ここが重要なポイントで、MultiReaderが作成された後に、元のスライスの最初の要素をnilに設定して変更します。
    • data, err := ioutil.ReadAll(r): MultiReaderからデータを読み込みます。
    • if err != nil || string(data) != "hello world": もしMultiReaderが元のスライスの変更に影響されていれば、nilになったReaderから読み込もうとしてエラーになるか、データが正しく読み込めないはずです。テストは、エラーがなく、期待通りのデータ("hello world")が読み込めることを検証します。これにより、MultiReaderがスライスのコピーを保持していることが証明されます。
  • TestMultiWriterCopy:

    • このテストは、MultiWriterが入力スライスをコピーし、その後の外部からの変更に対して影響を受けないことを確認します。
    • var buf bytes.Buffer: bytes.Bufferio.Writerインターフェースを実装しており、書き込まれたデータをメモリに保持します。
    • slice := []Writer{&buf}: bufを要素とするWriterスライスを定義します。
    • w := MultiWriter(slice...): このスライスからMultiWriterを作成します。
    • slice[0] = nil: MultiWriterが作成された後に、元のスライスの最初の要素をnilに設定して変更します。
    • n, err := w.Write([]byte("hello world")): MultiWriterにデータを書き込みます。
    • if err != nil || n != 11: エラーがなく、期待通りのバイト数(11)が書き込まれたことを確認します。
    • if buf.String() != "hello world": MultiWriterが元のスライスの変更に影響されていれば、nilになったWriterには書き込めず、bufの内容が空になるはずです。テストは、bufに期待通りのデータ("hello world")が書き込まれていることを検証します。これにより、MultiWriterがスライスのコピーを保持していることが証明されます。

これらのテストは、修正が正しく機能し、MultiReaderMultiWriterがより堅牢になったことを明確に示しています。

関連リンク

参考にした情報源リンク