[インデックス 19342] ファイルの概要
このコミットは、Go言語の標準ライブラリio
パッケージ内のMultiReader
およびMultiWriter
関数における重要な修正を導入しています。具体的には、これらの関数に渡されるReader
またはWriter
のスライスが、関数内部でコピーされるように変更されました。これにより、元のスライスが後から変更されても、MultiReader
やMultiWriter
の動作に予期せぬ影響が出ないようになります。
コミット
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
このメッセージは、MultiReader
とMultiWriter
が引数として受け取るスライスをコピーするように変更されたことを簡潔に示しています。また、関連する変更リスト(CL)や修正されたIssue番号(#7809)も記載されています。
変更の背景
この変更の背景には、io.MultiReader
およびio.MultiWriter
の以前の実装における潜在的なバグがありました。これらの関数は可変長引数(...Reader
や...Writer
)を受け取り、内部でそれらをスライスとして扱います。しかし、以前の実装では、このスライスが内部で直接参照されており、コピーが作成されていませんでした。
この「参照渡し」の挙動は、以下のような問題を引き起こす可能性がありました。
- 予期せぬ動作の変更:
MultiReader
やMultiWriter
が作成された後で、それらに渡された元のスライスが外部から変更された場合、MultiReader
やMultiWriter
の内部状態もその変更を反映してしまい、予期せぬ読み書きの挙動を示す可能性がありました。例えば、スライス内のReader
やWriter
がnil
に設定されたり、別のインスタンスに置き換えられたりすると、実行時エラーやデータの破損につながる恐れがありました。 - デバッグの困難さ: このような問題は、コードの異なる部分でスライスが共有され、非同期的に変更される場合に特にデバッグが困難になります。
このコミットは、Issue #7809で報告された問題を解決するために導入されました。この問題は、MultiReader
が作成された後に、その基になるスライスが変更されると、MultiReader
が正しく動作しないというものでした。
前提知識の解説
このコミットを理解するためには、以下のGo言語の概念とio
パッケージの基本的な知識が必要です。
-
スライス (Slice): Go言語のスライスは、配列のセグメントを参照するデータ構造です。スライス自体は、基になる配列へのポインタ、長さ(
len
)、容量(cap
)の3つの要素で構成されます。スライスを関数に渡す際、スライスヘッダ(ポインタ、長さ、容量)は値渡しされますが、そのポインタが指す基になる配列のデータは共有されます。したがって、関数内でスライスの要素を変更すると、元のスライスの基になる配列のデータも変更されます。 この特性が、本コミットで修正される問題の根本原因でした。 -
可変長引数 (Variadic Functions): Go言語では、関数の最後の引数に
...
を付けることで、その関数が0個以上の引数を受け取れるように定義できます。例えば、func MultiReader(readers ...Reader) Reader
のように定義された場合、readers
は関数内で[]Reader
型のスライスとして扱われます。 -
io.Reader
インターフェース:io.Reader
は、Go言語でデータを読み込むための基本的なインターフェースです。type Reader interface { Read(p []byte) (n int, err error) }
Read
メソッドは、バイトスライスp
にデータを読み込み、読み込んだバイト数n
とエラーerr
を返します。 -
io.Writer
インターフェース:io.Writer
は、Go言語でデータを書き込むための基本的なインターフェースです。type Writer interface { Write(p []byte) (n int, err error) }
Write
メソッドは、バイトスライスp
からデータを書き込み、書き込んだバイト数n
とエラーerr
を返します。 -
io.MultiReader
関数: 複数のio.Reader
を結合し、それらをあたかも単一のio.Reader
であるかのように扱うための関数です。MultiReader
からRead
を呼び出すと、内部的に最初のReader
から読み込みを試み、それがEOFに達すると次のReader
から読み込みを続けます。 -
io.MultiWriter
関数: 複数のio.Writer
を結合し、それらすべてに同じデータを書き込むための関数です。MultiWriter
にWrite
を呼び出すと、内部的にすべての登録されたWriter
に対して同じデータを書き込みます。これはUnixのtee
コマンドに似ています。 -
copy
関数: Go言語の組み込み関数copy(dst, src []Type)
は、src
スライスからdst
スライスに要素をコピーします。コピーされる要素数は、len(dst)
とlen(src)
の小さい方になります。この関数は、スライスの内容を別のメモリ領域に複製するために使用されます。
技術的詳細
このコミットの技術的な核心は、MultiReader
とMultiWriter
のコンストラクタ関数内で、入力として受け取ったReader
またはWriter
のスライスを明示的にコピーする点にあります。
以前の実装では、以下のようになっていました。
// 変更前 (概念的なコード)
func MultiReader(readers ...Reader) Reader {
return &multiReader{readers} // readersスライスが直接参照される
}
func MultiWriter(writers ...Writer) Writer {
return &multiWriter{writers} // writersスライスが直接参照される
}
このコードでは、multiReader
構造体やmultiWriter
構造体の内部フィールドreaders
やwriters
が、引数として渡されたスライスreaders
やwriters
と同じ基になる配列を共有していました。
このコミットによって、以下のように変更されました。
// 変更後
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} // コピーされたスライスを参照
}
この変更により、MultiReader
やMultiWriter
が内部で保持するスライスは、コンストラクタが呼び出された時点での入力スライスの独立したコピーになります。したがって、コンストラクタ呼び出し後に元のスライスが外部で変更されても、MultiReader
やMultiWriter
の内部状態には影響が及ばなくなり、より堅牢で予測可能な動作が保証されます。
この修正は、Go言語の「値渡し」と「参照渡し」のセマンティクス、特にスライスがどのように扱われるかについての理解を深める上で重要です。スライスヘッダは値渡しされますが、そのヘッダが指す基になる配列は共有されるため、意図しない副作用を避けるためには、必要に応じて明示的なコピーを行う必要があります。このコミットは、まさにその原則をio
パッケージの重要な関数に適用したものです。
コアとなるコードの変更箇所
変更はsrc/pkg/io/multi.go
とsrc/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}
として、コピーされたスライスr
がmultiReader
構造体に渡されます。これにより、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.Buffer
はio.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
がスライスのコピーを保持していることが証明されます。
- このテストは、
これらのテストは、修正が正しく機能し、MultiReader
とMultiWriter
がより堅牢になったことを明確に示しています。
関連リンク
- Go Issue #7809: https://github.com/golang/go/issues/7809
- Go CL 94380043: https://golang.org/cl/94380043
参考にした情報源リンク
- Go言語の
io
パッケージドキュメント: https://pkg.go.dev/io - Go言語のスライスに関する公式ブログ記事: https://go.dev/blog/slices
- Go言語における
io.MultiReader
とスライスの挙動に関する議論 (Web検索結果より):- https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQGzsL4EAcK3BacDzv0h26RHGQ32fJcD5FeD5TvG0M1hahjfJ2vpG4LKQZKeBsb8nOTte-PQSI1BghHcO63AXVguKgkNMvBS3BMFamPgy4SZ_iLCpHwlxCkErSccsnuU55xCkFVWWo1-6j9dJgSq_1PpgWHCOztoWA==
- https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQEu7foLg2xe7zw9yZTc32arXRnwthkMjW5XijARWJzzWR1-BkFf0MVEILM07DlautpMg2KeKNoVgpY9qkdb8cm12Y_ZrtxO8tnH-yun3_1k5dnVShDJfJjvhwutCwGJhkXg761RX93_wDpo1XTnySjE23XnkWpuqj9PN7j2FCb_AA==
- Go言語における
io.MultiWriter
とスライスの挙動に関する議論 (Web検索結果より):- https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQEts6Mr3EJaN2JgkBVH07aPVvTZ89QI6ILVdg6mF36-fSiTqhiEQi_niK0b-LAulXgcHKwnnRohwW1jdFH7iyfAoToojjmDTVzJcXX22WLSx9nrGK-h2M7ayXN6iHJxddyNFx8ybZyi6irq7-u3ZXj32l6KLMosZvvFhmYqVWErEW9KgRhgbS7gd7MFhndlu-MMqL6Tvg==
- https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQEC6ibixIlNI7M26m5ZRA_jCBpp1Xr9h9Zkvey2xmBA-H2Ufd_0uMHloFnXojrANbyfgozf4pggB7Au5eYC_mD33I1NPAI1DA3mUOnYU75lfg==
- https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQH6G4awN0KYaZBOhez3ofVc0wH5lE4GZOc3X3S66YMZ4WVpi4a7_UIFIIGWk_qOojvdtR5qmd9RAKRp8LvfVwZfYp_8HHc_-BIj3IiUH9wjlCbXfPuzCAWuG19C9P5x7w==
- https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQENUFZnArPU1Zue2_v-YkwKJRp9WHvCVJBdHthQMk5N5027KbSEdymHuNoihmKWzIq16IA8rJ-N26TWSluI4jIgzfNZfqPDvcv6waOAKnvwtCxPLfE2r_qCx4YFrUeGATH1SfoZQgLTZMZ8dJibTPlW1a09VqLJfKj216EnjYrK_CvBJesh4hDlgiyBsvrwRY_OwqUjUQ==