[インデックス 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)
という関数が存在し、これは与えられた文字列 s
を io.Reader
インターフェースを満たす *strings.Reader
型としてラップして返します。この *strings.Reader
は、io.Reader
だけでなく io.ByteReader
や io.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
を返します。この Reader
は io.Reader
、io.ByteReader
、io.RuneReader
、io.Seeker
、io.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の標準ライブラリの一部として十分にテストされ、最適化されています。
変更のメリット
- コードの簡素化と重複の排除: カスタムの
stringReader
の実装が不要になり、テストコードから約30行のコードが削除されました。これにより、コードベースがスリム化され、メンテナンスの負担が軽減されます。 - 標準ライブラリへの準拠: Goの標準ライブラリが提供する機能を使用することで、コードがよりGoらしい(idiomatic)ものになります。これは、他のGo開発者にとって理解しやすく、Goエコシステム全体での一貫性を保つ上で重要です。
- 信頼性とパフォーマンス:
strings.NewReader
はGoの標準ライブラリの一部であり、広範なテストと最適化が施されています。カスタム実装よりも信頼性が高く、多くの場合でパフォーマンスも優れています。 - 機能の拡張性:
strings.Reader
はio.Reader
だけでなく、io.ByteReader
、io.RuneReader
、io.Seeker
など、より多くのインターフェースを実装しています。これにより、将来的にテストでこれらの追加機能が必要になった場合でも、既存のコードを変更することなく対応できる可能性があります。
コアとなるコードの変更箇所
このコミットでは、主に以下の3つのテストファイルが変更されています。
src/pkg/encoding/xml/embed_test.go
src/pkg/encoding/xml/read_test.go
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つの主要な変更が行われました。
-
カスタム
stringReader
型とStringReader
関数の削除:src/pkg/encoding/xml/xml_test.go
ファイル内に定義されていたstringReader
という構造体と、それに関連するRead
およびReadByte
メソッド、そしてstringReader
のインスタンスを生成するヘルパー関数StringReader
が削除されました。これらのコードは、文字列をio.Reader
インターフェースとしてラップするためのものでしたが、これはstrings.NewReader
が提供する機能と重複していました。 -
StringReader(...)
の呼び出しをstrings.NewReader(...)
に置換:embed_test.go
、read_test.go
、xml_test.go
の各テストファイル内で、XMLデータを文字列リテラルとして定義し、それをUnmarshal
関数やNewParser
関数に渡す際に、これまでStringReader(xmlString)
の形式でカスタムのリーダーを作成していました。このコミットでは、これらの呼び出しがすべてstrings.NewReader(xmlString)
に変更されました。これにより、Go標準ライブラリのstrings
パッケージが提供する、より標準的で効率的なio.Reader
実装が利用されるようになりました。
この変更は、機能的な振る舞いを変更することなく、テストコードの内部実装を改善するものです。テストの正確性や実行結果に影響を与えることなく、コードの品質と保守性を向上させています。
関連リンク
- Go言語
io
パッケージのドキュメント: https://pkg.go.dev/io - Go言語
strings
パッケージのドキュメント: https://pkg.go.dev/strings - Go言語
strings.NewReader
のドキュメント: https://pkg.go.dev/strings#NewReader - Go言語
encoding/xml
パッケージのドキュメント: https://pkg.go.dev/encoding/xml
参考にした情報源リンク
- Go言語の公式ドキュメント (上記関連リンクに記載)
- Gitコミットの差分情報 (
git diff
) - Go言語のソースコード (
src/pkg/encoding/xml/
) - Go言語の
io.Reader
およびstrings.NewReader
に関する一般的な知識