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

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

このコミットは、Go言語の標準ライブラリである encoding/xml パッケージのテストコードにおいて、文字列からデータを読み込むためのカスタム実装 StringReader を廃止し、Go標準ライブラリの strings パッケージが提供する strings.NewReader 関数に置き換える変更です。これにより、テストコードの簡潔性、標準ライブラリへの準拠、および保守性の向上が図られています。

コミット

commit 38ff98b4c671dfe237a1737308af0a9de871c8c3
Author: Michael Shields <mshields@google.com>
Date:   Tue Jan 3 12:22:02 2012 +1100

    encoding/xml: use strings.Reader in tests.
    
    R=golang-dev, adg
    CC=golang-dev
    https://golang.org/cl/5502083

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

https://github.com/golang/go/commit/38ff98b4c671dfe237a1737308af0a9de871c8c3

元コミット内容

encoding/xml: use strings.Reader in tests.

このコミットメッセージは、encoding/xml パッケージのテストにおいて、strings.Reader を使用するように変更したことを簡潔に示しています。

変更の背景

Go言語の io パッケージは、I/O操作のためのプリミティブなインターフェースを提供します。特に io.Reader インターフェースは、データの読み込み操作を抽象化するための基本的なインターフェースです。多くのGoの関数やメソッドは、この io.Reader インターフェースを受け入れることで、様々なデータソース(ファイル、ネットワーク接続、メモリ上の文字列など)から透過的にデータを読み込むことができます。

encoding/xml パッケージのテストコードでは、XML文字列をパースするために、その文字列を io.Reader として扱う必要がありました。以前は、この目的のために stringReader という独自の型と、それを作成する StringReader 関数が src/pkg/encoding/xml/xml_test.go 内に定義されていました。

しかし、Go標準ライブラリの strings パッケージには、既に strings.NewReader(s string) という関数が存在し、これは与えられた文字列 sio.Reader インターフェースを満たす *strings.Reader 型としてラップして返します。この *strings.Reader は、io.Reader だけでなく io.ByteReaderio.Seeker など、より多くのインターフェースも実装しています。

このコミットの背景には、カスタム実装を標準ライブラリの機能に置き換えることで、コードの重複を避け、標準的なアプローチを採用し、将来的なメンテナンスコストを削減するという意図があります。標準ライブラリの機能を使用することで、コードの可読性が向上し、Goエコシステム全体での一貫性が保たれます。

前提知識の解説

Go言語の io パッケージと io.Reader インターフェース

Go言語の io パッケージは、入出力プリミティブを提供します。その中でも io.Reader インターフェースは非常に重要です。

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

Read メソッドは、最大 len(p) バイトのデータを p に読み込み、読み込んだバイト数 n とエラー err を返します。データがもうない場合は io.EOF エラーを返します。このインターフェースを実装することで、任意のデータソースを「読み込み可能」なものとして扱うことができます。

Go言語の strings パッケージと strings.NewReader

strings パッケージは、UTF-8でエンコードされた文字列を操作するためのシンプルな関数を提供します。 strings.NewReader 関数は、文字列を io.Reader として扱うための便利な方法を提供します。

func NewReader(s string) *Reader

NewReader は、文字列 s を読み込む新しい Reader を返します。この Readerio.Readerio.ByteReaderio.RuneReaderio.Seekerio.WriterTo インターフェースを実装しています。これにより、文字列をファイルやネットワークストリームと同じように扱うことが可能になります。

Go言語のテストにおける io.Reader の利用

Goのテストでは、特定の関数やメソッドが io.Reader を引数として取る場合、テストデータを文字列リテラルとして定義し、それを io.Reader に変換して関数に渡すことがよくあります。これにより、テストのセットアップが簡潔になり、ファイルI/Oなどの外部依存を排除できます。

技術的詳細

このコミットの技術的な核心は、カスタムで実装されていた stringReader 型と StringReader 関数が、Go標準ライブラリの strings.NewReader 関数に置き換えられた点にあります。

変更前 (stringReader の実装)

変更前は、src/pkg/encoding/xml/xml_test.go 内に以下のようなカスタム実装がありました。

type stringReader struct {
	s   string
	off int
}

func (r *stringReader) Read(b []byte) (n int, err error) {
	if r.off >= len(r.s) {
		return 0, io.EOF
	}
	for r.off < len(r.s) && n < len(b) {
		b[n] = r.s[r.off]
		n++
		r.off++
	}
	return
}

func (r *stringReader) ReadByte() (b byte, err error) {
	if r.off >= len(r.s) {
		return 0, io.EOF
	}
	b = r.s[r.off]
	r.off++
	return
}

func StringReader(s string) io.Reader { return &stringReader{s, 0} }

このコードは、文字列をバイトスライスとして読み込む Read メソッドと、1バイトずつ読み込む ReadByte メソッドを実装することで、io.Reader および io.ByteReader インターフェースを満たしていました。StringReader 関数は、このカスタム stringReader のインスタンスを生成して返していました。

変更後 (strings.NewReader の利用)

変更後は、このカスタム実装が削除され、代わりに strings.NewReader が使用されます。

例えば、Unmarshal(StringReader(_1a), &a) のような呼び出しは、Unmarshal(strings.NewReader(_1a), &a) に変更されました。

strings.NewReader は、内部的に strings.Reader 型のポインタを返します。この strings.Reader 型は、io.Reader インターフェースを効率的に実装しており、Goの標準ライブラリの一部として十分にテストされ、最適化されています。

変更のメリット

  1. コードの簡素化と重複の排除: カスタムの stringReader の実装が不要になり、テストコードから約30行のコードが削除されました。これにより、コードベースがスリム化され、メンテナンスの負担が軽減されます。
  2. 標準ライブラリへの準拠: Goの標準ライブラリが提供する機能を使用することで、コードがよりGoらしい(idiomatic)ものになります。これは、他のGo開発者にとって理解しやすく、Goエコシステム全体での一貫性を保つ上で重要です。
  3. 信頼性とパフォーマンス: strings.NewReader はGoの標準ライブラリの一部であり、広範なテストと最適化が施されています。カスタム実装よりも信頼性が高く、多くの場合でパフォーマンスも優れています。
  4. 機能の拡張性: strings.Readerio.Reader だけでなく、io.ByteReaderio.RuneReaderio.Seeker など、より多くのインターフェースを実装しています。これにより、将来的にテストでこれらの追加機能が必要になった場合でも、既存のコードを変更することなく対応できる可能性があります。

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

このコミットでは、主に以下の3つのテストファイルが変更されています。

  1. src/pkg/encoding/xml/embed_test.go
  2. src/pkg/encoding/xml/read_test.go
  3. src/pkg/encoding/xml/xml_test.go

具体的な変更内容は以下の通りです。

src/pkg/encoding/xml/embed_test.go

  • import "strings" が追加されました。
  • Unmarshal(StringReader(...), ...) の呼び出しが Unmarshal(strings.NewReader(...), ...) に変更されました。
--- a/src/pkg/encoding/xml/embed_test.go
+++ b/src/pkg/encoding/xml/embed_test.go
@@ -4,7 +4,10 @@
 
  package xml
 
-import "testing"
+import (
+	"strings"
+	"testing"
+)
 
  type C struct {
  	Name string
@@ -41,7 +44,7 @@ const _1a = `
  // Tests that embedded structs are marshalled.
  func TestEmbedded1(t *testing.T) {
  	var a A
-	if e := Unmarshal(StringReader(_1a), &a); e != nil {
+	if e := Unmarshal(strings.NewReader(_1a), &a); e != nil {
  		t.Fatalf("Unmarshal: %s", e)
  	}
  	if a.FieldA != "foo" {
@@ -80,7 +83,7 @@ const _2a = `
  // Tests that conflicting field names get excluded.
  func TestEmbedded2(t *testing.T) {
  	var a A2
-	if e := Unmarshal(StringReader(_2a), &a); e != nil {
+	if e := Unmarshal(strings.NewReader(_2a), &a); e != nil {
  		t.Fatalf("Unmarshal: %s", e)
  	}
  	if a.XY != "" {
@@ -99,7 +102,7 @@ type A3 struct {
  // Tests that private fields are not set.
  func TestEmbedded3(t *testing.T) {
  	var a A3
-	if e := Unmarshal(StringReader(_2a), &a); e != nil {
+	if e := Unmarshal(strings.NewReader(_2a), &a); e != nil {
  		t.Fatalf("Unmarshal: %s", e)
  	}
  	if a.xy != "" {
@@ -115,7 +118,7 @@ type A4 struct {
  // Tests that private fields are not set.
  func TestEmbedded4(t *testing.T) {
  	var a A4
-	if e := Unmarshal(StringReader(_2a), &a); e != nil {
+	if e := Unmarshal(strings.NewReader(_2a), &a); e != nil {
  		t.Fatalf("Unmarshal: %s", e)
  	}
  	if a.Any != "foo" {

src/pkg/encoding/xml/read_test.go

  • import "strings" が追加されました。
  • Unmarshal(StringReader(...), ...) の呼び出しが Unmarshal(strings.NewReader(...), ...) に変更されました。
--- a/src/pkg/encoding/xml/read_test.go
+++ b/src/pkg/encoding/xml/read_test.go
@@ -6,6 +6,7 @@ package xml
 
  import (
  	"reflect"
+	"strings"
  	"testing"
  )
 
@@ -13,7 +14,7 @@ import (
 
  func TestUnmarshalFeed(t *testing.T) {
  	var f Feed
-	if err := Unmarshal(StringReader(atomFeedString), &f); err != nil {
+	if err := Unmarshal(strings.NewReader(atomFeedString), &f); err != nil {
  		t.Fatalf("Unmarshal: %s", err)
  	}
  	if !reflect.DeepEqual(f, atomFeed) {
@@ -298,7 +299,7 @@ var pathTests = []interface{}{\
  func TestUnmarshalPaths(t *testing.T) {
  	for _, pt := range pathTests {
  		v := reflect.New(reflect.TypeOf(pt).Elem()).Interface()
-		if err := Unmarshal(StringReader(pathTestString), v); err != nil {
+		if err := Unmarshal(strings.NewReader(pathTestString), v); err != nil {
  			t.Fatalf("Unmarshal: %s", err)
  		}
  		if !reflect.DeepEqual(v, pt) {
@@ -328,7 +329,7 @@ var badPathTests = []struct {\
 
  func TestUnmarshalBadPaths(t *testing.T) {
  	for _, tt := range badPathTests {
-		err := Unmarshal(StringReader(pathTestString), tt.v)
+		err := Unmarshal(strings.NewReader(pathTestString), tt.v)
  		if !reflect.DeepEqual(err, tt.e) {
  			t.Fatalf("Unmarshal with %#v didn't fail properly: %#v", tt.v, err)
  		}
@@ -337,7 +338,7 @@ func TestUnmarshalBadPaths(t *testing.T) {\
 
  func TestUnmarshalAttrs(t *testing.T) {
  	var f AttrTest
-	if err := Unmarshal(StringReader(attrString), &f); err != nil {
+	if err := Unmarshal(strings.NewReader(attrString), &f); err != nil {
  		t.Fatalf("Unmarshal: %s", err)
  	}
  	if !reflect.DeepEqual(f, attrStruct) {
@@ -393,7 +394,7 @@ type TestThree struct {\
 
  func TestUnmarshalWithoutNameType(t *testing.T) {
  	var x TestThree
-	if err := Unmarshal(StringReader(withoutNameTypeData), &x); err != nil {
+	if err := Unmarshal(strings.NewReader(withoutNameTypeData), &x); err != nil {
  		t.Fatalf("Unmarshal: %s", err)
  	}
  	if x.Attr != OK {

src/pkg/encoding/xml/xml_test.go

  • stringReader 型、Read メソッド、ReadByte メソッド、および StringReader 関数が完全に削除されました。
  • NewParser(StringReader(...)) の呼び出しが NewParser(strings.NewReader(...)) に変更されました。
--- a/src/pkg/encoding/xml/xml_test.go
+++ b/src/pkg/encoding/xml/xml_test.go
@@ -154,36 +154,8 @@ var xmlInput = []string{\
  	"<t>cdata]]></t>",
  }\
 
-type stringReader struct {
-	s   string
-	off int
-}
-
-func (r *stringReader) Read(b []byte) (n int, err error) {
-	if r.off >= len(r.s) {
-		return 0, io.EOF
-	}
-	for r.off < len(r.s) && n < len(b) {
-		b[n] = r.s[r.off]
-		n++
-		r.off++
-	}
-	return
-}
-
-func (r *stringReader) ReadByte() (b byte, err error) {
-	if r.off >= len(r.s) {
-		return 0, io.EOF
-	}
-	b = r.s[r.off]
-	r.off++
-	return
-}
-
-func StringReader(s string) io.Reader { return &stringReader{s, 0} }
-
  func TestRawToken(t *testing.T) {\
-	p := NewParser(StringReader(testInput))\
+	p := NewParser(strings.NewReader(testInput))\
  	testRawToken(t, p, rawTokens)
  }
 
@@ -207,7 +179,7 @@ func (d *downCaser) Read(p []byte) (int, error) {\
 
  func TestRawTokenAltEncoding(t *testing.T) {\
  	sawEncoding := ""
-	p := NewParser(StringReader(testInputAltEncoding))\
+	p := NewParser(strings.NewReader(testInputAltEncoding))\
  	p.CharsetReader = func(charset string, input io.Reader) (io.Reader, error) {
  		sawEncoding = charset
  		if charset != "x-testing-uppercase" {
@@ -219,7 +191,7 @@ func TestRawTokenAltEncoding(t *testing.T) {\
 }
 
  func TestRawTokenAltEncodingNoConverter(t *testing.T) {\
-	p := NewParser(StringReader(testInputAltEncoding))\
+	p := NewParser(strings.NewReader(testInputAltEncoding))\
  	token, err := p.RawToken()
  	if token == nil {
  		t.Fatalf("expected a token on first RawToken call")
@@ -286,7 +258,7 @@ var nestedDirectivesTokens = []Token{\
 }\
 
  func TestNestedDirectives(t *testing.T) {\
-	p := NewParser(StringReader(nestedDirectivesInput))\
+	p := NewParser(strings.NewReader(nestedDirectivesInput))\
 
  	for i, want := range nestedDirectivesTokens {\
  		have, err := p.Token()
@@ -300,7 +272,7 @@ func TestNestedDirectives(t *testing.T) {\
 }\
 
  func TestToken(t *testing.T) {\
-	p := NewParser(StringReader(testInput))\
+	p := NewParser(strings.NewReader(testInput))\
 
  	for i, want := range cookedTokens {\
  		have, err := p.Token()
@@ -315,7 +287,7 @@ func TestToken(t *testing.T) {\
 
  func TestSyntax(t *testing.T) {\
  	for i := range xmlInput {\
-		p := NewParser(StringReader(xmlInput[i]))\
+		p := NewParser(strings.NewReader(xmlInput[i]))\
  		var err error
  		for _, err = p.Token(); err == nil; _, err = p.Token() {
  		}\
@@ -424,7 +396,7 @@ func TestIssue569(t *testing.T) {\
 
  func TestUnquotedAttrs(t *testing.T) {\
  	data := "<tag attr=azAZ09:-_\\t>"\
-	p := NewParser(StringReader(data))\
+	p := NewParser(strings.NewReader(data))\
  	p.Strict = false
  	token, err := p.Token()
  	if _, ok := err.(*SyntaxError); ok {
@@ -450,7 +422,7 @@ func TestValuelessAttrs(t *testing.T) {\
  		{"<input checked />", "input", "checked"},
  	}\
  	for _, test := range tests {\
-		p := NewParser(StringReader(test[0]))\
+		p := NewParser(strings.NewReader(test[0]))\
  		p.Strict = false
  		token, err := p.Token()
  		if _, ok := err.(*SyntaxError); ok {
@@ -500,7 +472,7 @@ func TestCopyTokenStartElement(t *testing.T) {\
 
  func TestSyntaxErrorLineNum(t *testing.T) {\
  	testInput := "<P>Foo<P>\\n\\n<P>Bar</>\\n"\
-	p := NewParser(StringReader(testInput))\
+	p := NewParser(strings.NewReader(testInput))\
  	var err error
  	for _, err = p.Token(); err == nil; _, err = p.Token() {
  	}\
@@ -515,7 +487,7 @@ func TestSyntaxErrorLineNum(t *testing.T) {\
 
  func TestTrailingRawToken(t *testing.T) {\
  	input := `<FOO></FOO>  `\
-	p := NewParser(StringReader(input))\
+	p := NewParser(strings.NewReader(input))\
  	var err error
  	for _, err = p.RawToken(); err == nil; _, err = p.RawToken() {
  	}\
@@ -526,7 +498,7 @@ func TestTrailingRawToken(t *testing.T) {\
 
  func TestTrailingToken(t *testing.T) {\
  	input := `<FOO></FOO>  `\
-	p := NewParser(StringReader(input))\
+	p := NewParser(strings.NewReader(input))\
  	var err error
  	for _, err = p.Token(); err == nil; _, err = p.Token() {
  	}\
@@ -537,7 +509,7 @@ func TestTrailingToken(t *testing.T) {\
 
  func TestEntityInsideCDATA(t *testing.T) {\
  	input := `<test><![CDATA[ &val=foo ]]></test>`\
-	p := NewParser(StringReader(input))\
+	p := NewParser(strings.NewReader(input))\
  	var err error
  	for _, err = p.Token(); err == nil; _, err = p.Token() {
  	}\
@@ -569,7 +541,7 @@ var characterTests = []struct {\
  func TestDisallowedCharacters(t *testing.T) {\
 
  	for i, tt := range characterTests {\
-		p := NewParser(StringReader(tt.in))\
+		p := NewParser(strings.NewReader(tt.in))\
  		var err error
 
  		for err == nil {

コアとなるコードの解説

このコミットのコアとなる変更は、encoding/xml パッケージのテストコード内で、文字列を io.Reader として扱うためのアプローチを、カスタム実装から標準ライブラリの機能へと移行した点です。

具体的には、以下の2つの主要な変更が行われました。

  1. カスタム stringReader 型と StringReader 関数の削除: src/pkg/encoding/xml/xml_test.go ファイル内に定義されていた stringReader という構造体と、それに関連する Read および ReadByte メソッド、そして stringReader のインスタンスを生成するヘルパー関数 StringReader が削除されました。これらのコードは、文字列を io.Reader インターフェースとしてラップするためのものでしたが、これは strings.NewReader が提供する機能と重複していました。

  2. StringReader(...) の呼び出しを strings.NewReader(...) に置換: embed_test.goread_test.goxml_test.go の各テストファイル内で、XMLデータを文字列リテラルとして定義し、それを Unmarshal 関数や NewParser 関数に渡す際に、これまで StringReader(xmlString) の形式でカスタムのリーダーを作成していました。このコミットでは、これらの呼び出しがすべて strings.NewReader(xmlString) に変更されました。これにより、Go標準ライブラリの strings パッケージが提供する、より標準的で効率的な io.Reader 実装が利用されるようになりました。

この変更は、機能的な振る舞いを変更することなく、テストコードの内部実装を改善するものです。テストの正確性や実行結果に影響を与えることなく、コードの品質と保守性を向上させています。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント (上記関連リンクに記載)
  • Gitコミットの差分情報 (git diff)
  • Go言語のソースコード (src/pkg/encoding/xml/)
  • Go言語の io.Reader および strings.NewReader に関する一般的な知識