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

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

このコミットは、Go言語の標準ライブラリである bufio パッケージのテストコード (bufio_test.go) における、不要な間接参照(ポインタの使用)を削除する変更です。具体的には、onlyReader および onlyWriter というテスト用の構造体のインスタンスを生成する際に、ポインタではなく値型を直接使用するように修正されています。これにより、テストコードの記述がより簡潔になり、わずかながらパフォーマンスの改善や可読性の向上が期待されます。

コミット

commit 0ba5ec53b047376e77418a248233b0645a79a838
Author: Nigel Tao <nigeltao@golang.org>
Date:   Sat Oct 20 13:02:29 2012 +1100

    bufio: remove a little unnecessary indirection in tests.
    
    R=mchaten, r
    CC=golang-dev
    https://golang.org/cl/6739045

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

https://github.com/golang/go/commit/0ba5ec53b047376e77418a248233b0645a79a838

元コミット内容

bufio: remove a little unnecessary indirection in tests.

R=mchaten, r
CC=golang-dev
https://golang.org/cl/6739045

変更の背景

このコミットの背景には、Go言語におけるインターフェースの実装と、ポインタレシーバおよび値レシーバの使い分けに関するベストプラクティスがあります。bufio パッケージのテストコードでは、io.Readerio.Writer インターフェースを実装する onlyReader および onlyWriter というヘルパ構造体が使用されていました。

元々のコードでは、これらの構造体のインスタンスを生成する際に &onlyReader{...}&onlyWriter{...} のようにポインタを使用していました。しかし、onlyReaderonlyWriterRead および Write メソッドは、それぞれ値レシーバ (func (r onlyReader) Read(...) および func (w onlyWriter) Write(...)) で定義されていました。

Go言語では、インターフェースを実装する型がポインタレシーバを持つメソッドを定義している場合、そのインターフェースを満たすのはポインタ型のみです。しかし、値レシーバを持つメソッドを定義している場合、そのインターフェースを満たすのは値型とポインタ型の両方です。

このケースでは、onlyReaderonlyWriter のメソッドが値レシーバで定義されているため、これらの構造体の値型 (onlyReader, onlyWriter) も io.Readerio.Writer インターフェースを直接満たします。したがって、&onlyReader{...} のようにポインタを介してインスタンスを生成する必要はなく、onlyReader{...} のように直接値型を生成しても問題ありませんでした。

この変更は、テストコードにおけるわずかながら不要な間接参照を排除し、コードの意図をより明確にするとともに、Goの型システムとインターフェースの振る舞いをより適切に反映させることを目的としています。

前提知識の解説

Go言語のインターフェース

Go言語のインターフェースは、メソッドのシグネチャの集合を定義する型です。型がインターフェースのすべてのメソッドを実装していれば、その型はそのインターフェースを満たします。Goのインターフェースは「暗黙的」に満たされるため、implements キーワードのような明示的な宣言は不要です。

io.Readerio.Writer インターフェース

Goの標準ライブラリ io パッケージには、データストリームの読み書きを抽象化するための基本的なインターフェースが定義されています。

  • io.Reader: データを読み込むためのインターフェースで、Read(p []byte) (n int, err error) メソッドを定義します。
  • io.Writer: データを書き込むためのインターフェースで、Write(p []byte) (n int, err error) メソッドを定義します。

これらのインターフェースは、ファイル、ネットワーク接続、メモリバッファなど、様々なデータソースやシンクに対して統一的なI/O操作を提供するために広く利用されます。

ポインタレシーバと値レシーバ

Go言語のメソッドは、レシーバ(メソッドが関連付けられる型)を値またはポインタで受け取ることができます。

  • 値レシーバ (func (t MyType) MyMethod(...)):

    • メソッドが呼び出される際、レシーバの値のコピーが渡されます。
    • メソッド内でレシーバのフィールドを変更しても、元の値には影響しません。
    • レシーバが大きな構造体の場合、コピーのコストが発生する可能性があります。
    • インターフェースを実装する場合、値型 (MyType) とそのポインタ型 (*MyType) の両方がインターフェースを満たします。
  • ポインタレシーバ (func (t *MyType) MyMethod(...)):

    • メソッドが呼び出される際、レシーバのポインタが渡されます。
    • メソッド内でレシーバのフィールドを変更すると、元の値に影響します。
    • レシーバが大きな構造体の場合でも、コピーのコストは発生しません。
    • インターフェースを実装する場合、そのポインタ型 (*MyType) のみがインターフェースを満たします。値型 (MyType) はインターフェースを満たしません(ただし、Go 1.18以降のジェネリクスではこの限りではありませんが、このコミットの時点では該当しません)。

このコミットの文脈では、onlyReaderonlyWriter のメソッドが値レシーバで定義されているため、これらの構造体の値型自体が io.Readerio.Writer インターフェースを満たしていました。

技術的詳細

このコミットの技術的な核心は、Go言語のインターフェースとレシーバの振る舞いを理解し、それに基づいてコードを最適化することにあります。

bufio_test.go 内で定義されている onlyReaderonlyWriter 構造体は、それぞれ io.Readerio.Writer インターフェースを実装しています。

type onlyReader struct {
	r io.Reader
}

// Read メソッドは値レシーバで定義されている
func (r onlyReader) Read(b []byte) (int, error) {
	return r.r.Read(b)
}

type onlyWriter struct {
	w io.Writer
}

// Write メソッドは値レシーバで定義されている
func (w onlyWriter) Write(b []byte) (int, error) {
	return w.w.Write(b)
}

Read メソッドと Write メソッドが値レシーバ (func (r onlyReader) Read(...) および func (w onlyWriter) Write(...)) で定義されているため、Goのルールにより、onlyReader 型のio.Reader インターフェースを満たします。同様に、onlyWriter 型のio.Writer インターフェースを満たします。

したがって、これらの構造体のインスタンスを生成してインターフェース型として使用する際に、&onlyReader{...} のようにポインタを取る必要はありませんでした。直接 onlyReader{...} のように値型を生成しても、インターフェースの要件を満たします。

この変更は、コンパイラが生成するコードに大きな影響を与えるものではありませんが、以下の点でメリットがあります。

  1. 可読性の向上: 不要な & 演算子を削除することで、コードがより簡潔になり、onlyReaderonlyWriter が値型として扱われていることが明確になります。
  2. 意図の明確化: onlyReaderonlyWriter が内部状態を変更しない(または変更する必要がない)ことを示唆し、値セマンティクスが適切であることを強調します。
  3. わずかなパフォーマンス改善(理論上): ポインタを介した間接参照が一つ減るため、ごくわずかながら実行時のオーバーヘッドが削減される可能性があります。ただし、現代のコンパイラはこのような最適化を自動で行うことが多いため、実測でのパフォーマンス差はほとんどないでしょう。

このコミットは、Go言語のイディオムに沿ったコードスタイルへの改善と見なすことができます。

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

変更は src/pkg/bufio/bufio_test.go ファイルのみで行われています。

具体的には、onlyReader および onlyWriter のインスタンスを生成する箇所で、& 演算子(アドレス演算子)が削除されています。

--- a/src/pkg/bufio/bufio_test.go
+++ b/src/pkg/bufio/bufio_test.go
@@ -779,7 +779,7 @@ func createTestInput(n int) []byte {
 
 func TestReaderWriteTo(t *testing.T) {
 	input := createTestInput(8192)
-	r := NewReader(&onlyReader{bytes.NewBuffer(input)})
+	r := NewReader(onlyReader{bytes.NewBuffer(input)})
 	w := new(bytes.Buffer)
 	if n, err := r.WriteTo(w); err != nil || n != int64(len(input)) {
 		t.Fatalf("r.WriteTo(w) = %d, %v, want %d, nil", n, err, len(input))
@@ -824,7 +824,7 @@ func TestReaderWriteToErrors(t *testing.T) {
 
 func TestWriterReadFrom(t *testing.T) {
 	ws := []func(io.Writer) io.Writer{
-		func(w io.Writer) io.Writer { return &onlyWriter{w} },
+		func(w io.Writer) io.Writer { return onlyWriter{w} },
 		func(w io.Writer) io.Writer { return w },
 	}
 
@@ -896,11 +896,11 @@ func TestWriterReadFromCounts(t *testing.T) {
 	if w0 != 0 {
 		t.Fatalf("write 1200 'x's: got %d writes, want 0", w0)
 	}
-	io.Copy(b0, &onlyReader{strings.NewReader(strings.Repeat("x", 30))})
+	io.Copy(b0, onlyReader{strings.NewReader(strings.Repeat("x", 30))})
 	if w0 != 0 {
 		t.Fatalf("write 1230 'x's: got %d writes, want 0", w0)
 	}
-	io.Copy(b0, &onlyReader{strings.NewReader(strings.Repeat("x", 9))})
+	io.Copy(b0, onlyReader{strings.NewReader(strings.Repeat("x", 9))})
 	if w0 != 1 {
 		t.Fatalf("write 1239 'x's: got %d writes, want 1", w0)
 	}
@@ -916,11 +916,11 @@ func TestWriterReadFromCounts(t *testing.T) {
 	if w1 != 1 {
 		t.Fatalf("write 1200 + 89 'x's: got %d writes, want 1", w1)
 	}
-	io.Copy(b1, &onlyReader{strings.NewReader(strings.Repeat("x", 700))})
+	io.Copy(b1, onlyReader{strings.NewReader(strings.Repeat("x", 700))})
 	if w1 != 1 {
 		t.Fatalf("write 1200 + 789 'x's: got %d writes, want 1", w1)
 	}
-	io.Copy(b1, &onlyReader{strings.NewReader(strings.Repeat("x", 600))})
+	io.Copy(b1, onlyReader{strings.NewReader(strings.Repeat("x", 600))})
 	if w1 != 2 {
 		t.Fatalf("write 1200 + 1389 'x's: got %d writes, want 2", w1)
 	}
@@ -944,7 +944,7 @@ type onlyReader struct {
 	r io.Reader
 }
 
-func (r *onlyReader) Read(b []byte) (int, error) {
+func (r onlyReader) Read(b []byte) (int, error) {
 	return r.r.Read(b)
 }
 
@@ -953,7 +953,7 @@ type onlyWriter struct {
 	w io.Writer
 }
 
-func (w *onlyWriter) Write(b []byte) (int, error) {
+func (w onlyWriter) Write(b []byte) (int, error) {
 	return w.w.Write(b)
 }
 
@@ -962,7 +962,7 @@ func BenchmarkReaderCopyOptimal(b *testing.B) {
 	for i := 0; i < b.N; i++ {
 		b.StopTimer()
 		src := NewReader(bytes.NewBuffer(make([]byte, 8192)))
-		dst := &onlyWriter{new(bytes.Buffer)}
+		dst := onlyWriter{new(bytes.Buffer)}
 		b.StartTimer()
 		io.Copy(dst, src)
 	}
@@ -972,8 +972,8 @@ func BenchmarkReaderCopyUnoptimal(b *testing.B) {
 	// Unoptimal case is where the underlying reader doesn't implement io.WriterTo
 	for i := 0; i < b.N; i++ {
 		b.StopTimer()
-		src := NewReader(&onlyReader{bytes.NewBuffer(make([]byte, 8192))})
-		dst := &onlyWriter{new(bytes.Buffer)}
+		src := NewReader(onlyReader{bytes.NewBuffer(make([]byte, 8192))})
+		dst := onlyWriter{new(bytes.Buffer)}
 		b.StartTimer()
 		io.Copy(dst, src)
 	}
@@ -982,8 +982,8 @@ func BenchmarkReaderCopyNoWriteTo(b *testing.B) {
 func BenchmarkReaderCopyNoWriteTo(b *testing.B) {
 	for i := 0; i < b.N; i++ {
 		b.StopTimer()
-		src := &onlyReader{NewReader(bytes.NewBuffer(make([]byte, 8192)))}\n
-		dst := &onlyWriter{new(bytes.Buffer)}\n
+		src := onlyReader{NewReader(bytes.NewBuffer(make([]byte, 8192)))}
+		dst := onlyWriter{new(bytes.Buffer)}
 		b.StartTimer()
 		io.Copy(dst, src)
 	}
@@ -993,7 +993,7 @@ func BenchmarkWriterCopyOptimal(b *testing.B) {
 	// Optimal case is where the underlying writer implements io.ReaderFrom
 	for i := 0; i < b.N; i++ {
 		b.StopTimer()
-		src := &onlyReader{bytes.NewBuffer(make([]byte, 8192))}\n
+		src := onlyReader{bytes.NewBuffer(make([]byte, 8192))}
 		dst := NewWriter(new(bytes.Buffer))
 		b.StartTimer()
 		io.Copy(dst, src)
@@ -1003,8 +1003,8 @@ func BenchmarkWriterCopyUnoptimal(b *testing.B) {
 func BenchmarkWriterCopyUnoptimal(b *testing.B) {
 	for i := 0; i < b.N; i++ {
 		b.StopTimer()
-		src := &onlyReader{bytes.NewBuffer(make([]byte, 8192))}\n
-		dst := NewWriter(&onlyWriter{new(bytes.Buffer)})\n
+		src := onlyReader{bytes.NewBuffer(make([]byte, 8192))}
+		dst := NewWriter(onlyWriter{new(bytes.Buffer)})
 		b.StartTimer()
 		io.Copy(dst, src)
 	}
@@ -1013,8 +1013,8 @@ func BenchmarkWriterCopyNoReadFrom(b *testing.B) {
 func BenchmarkWriterCopyNoReadFrom(b *testing.B) {
 	for i := 0; i < b.N; i++ {
 		b.StopTimer()
-		src := &onlyReader{bytes.NewBuffer(make([]byte, 8192))}\n
-		dst := &onlyWriter{NewWriter(new(bytes.Buffer))}\n
+		src := onlyReader{bytes.NewBuffer(make([]byte, 8192))}
+		dst := onlyWriter{NewWriter(new(bytes.Buffer))}
 		b.StartTimer()
 		io.Copy(dst, src)
 	}

コアとなるコードの解説

変更の核心は、onlyReaderonlyWriter の構造体リテラルを初期化する際に、ポインタ (&) を使用しないようにした点です。

変更前:

r := NewReader(&onlyReader{bytes.NewBuffer(input)})
// ...
func(w io.Writer) io.Writer { return &onlyWriter{w} },
// ...
io.Copy(b0, &onlyReader{strings.NewReader(strings.Repeat("x", 30))})
// ...
func (r *onlyReader) Read(b []byte) (int, error) { // ポインタレシーバ
	return r.r.Read(b)
}
// ...
func (w *onlyWriter) Write(b []byte) (int, error) { // ポインタレシーバ
	return w.w.Write(b)
}

変更後:

r := NewReader(onlyReader{bytes.NewBuffer(input)})
// ...
func(w io.Writer) io.Writer { return onlyWriter{w} },
// ...
io.Copy(b0, onlyReader{strings.NewReader(strings.Repeat("x", 30))})
// ...
func (r onlyReader) Read(b []byte) (int, error) { // 値レシーバ
	return r.r.Read(b)
}
// ...
func (w onlyWriter) Write(b []byte) (int, error) { // 値レシーバ
	return w.w.Write(b)
}

注目すべきは、onlyReaderonlyWriterRead および Write メソッドのレシーバが、このコミットでポインタレシーバから値レシーバに変更されている点です。

  • 変更前: func (r *onlyReader) Read(...) および func (w *onlyWriter) Write(...)
  • 変更後: func (r onlyReader) Read(...) および func (w onlyWriter) Write(...)

このレシーバの変更が、インスタンス生成時の & 演算子削除を可能にしています。

Go言語のインターフェースのルールでは、メソッドがポインタレシーバで定義されている場合、そのインターフェースを満たすのはポインタ型のみです。しかし、メソッドが値レシーバで定義されている場合、そのインターフェースを満たすのは値型とポインタ型の両方です。

したがって、onlyReaderonlyWriterRead/Write メソッドが値レシーバになったことで、onlyReader{...}onlyWriter{...} といった値型そのものが io.Readerio.Writer インターフェースを満たすようになりました。これにより、不要なポインタの生成を避けることができ、コードがより直接的で簡潔になっています。

これは、Goのイディオムに沿った改善であり、テストコードの可読性と保守性を向上させるための細かな調整です。

関連リンク

参考にした情報源リンク