[インデックス 15274] ファイルの概要
このコミットは、Go言語の標準ライブラリである encoding/binary
パッケージにおけるエラーハンドリングの改善に関するものです。encoding/binary
パッケージは、Goのデータ構造とバイトシーケンスの間で変換を行う機能を提供します。具体的には、binary.Read
や binary.Write
といった関数を通じて、Goのプリミティブ型や構造体をバイナリ形式にエンコード/デコードする際に使用されます。
この変更の主な目的は、encoding/binary
パッケージがサポートしていない型(例えば、固定長ではない型や、バイナリエンコードに適さない型)が Read
や Write
関数に渡された際に発生するエラーメッセージを、より具体的で分かりやすいものにすることです。以前は「無効な型 S」といった一般的なメッセージが表示されていましたが、このコミットにより、構造体内のどのフィールドが問題を引き起こしているのかを特定できるようになりました。
コミット
encoding/binary
パッケージにおいて、型エラーのメッセージをより具体的にする変更です。
以前は構造体 S
に対して「無効な型 S」と表示されていましたが、
変更後は構造体内のどの型が問題であるかを明示するようになりました。
Issue #4825 を修正します。
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/2b9787c2f3861736e82aa715343b67157911917f
元コミット内容
commit 2b9787c2f3861736e82aa715343b67157911917f
Author: Russ Cox <rsc@golang.org>
Date: Fri Feb 15 13:12:28 2013 -0500
encoding/binary: make type error more specific
Right now it says 'invalid type S' for a struct type S.
Instead, say which type inside the struct is the problem.
Fixes #4825.
R=golang-dev, iant
CC=golang-dev
https://golang.org/cl/7301102
変更の背景
encoding/binary
パッケージは、Goのデータ構造をバイナリ形式に変換する際に、固定長の型(例: int8
, float32
など)や、それらを含む構造体、スライスなどを扱います。しかし、このパッケージがサポートしていない型(例: string
、可変長のスライス、ポインタ、チャネル、関数など)を Read
や Write
関数に渡した場合、エラーが発生します。
このコミット以前は、例えば以下のような構造体を binary.Write
に渡した場合、エラーメッセージが非常に一般的でした。
type MyStruct struct {
Value1 int32
Value2 string // stringはbinaryエンコードできない
}
// binary.Write(w, binary.BigEndian, MyStruct{}) を呼び出すと
// "binary.Write: invalid type MyStruct" のようなエラーが出力されていた。
このエラーメッセージでは、MyStruct
全体が問題であるとしか示されず、具体的に MyStruct
のどのフィールド(この場合は Value2 string
)が問題を引き起こしているのかが分かりませんでした。これはデバッグを困難にし、開発者が問題の原因を特定するのに時間を要する原因となっていました。
この問題を解決するため、Issue #4825 が提起されました。このコミットは、このIssueを修正し、エラーメッセージに問題のある具体的な型情報を含めることで、デバッグの効率を向上させることを目的としています。
前提知識の解説
encoding/binary
パッケージ
encoding/binary
パッケージは、Goのデータとバイトシーケンスの間で変換を行うための機能を提供します。主に、ネットワークプロトコルやファイルフォーマットなど、固定長のバイナリデータを扱う際に利用されます。
binary.Read(r io.Reader, order ByteOrder, data interface{}) error
:io.Reader
からバイナリデータを読み込み、指定されたバイトオーダー (ByteOrder
) に従ってdata
インターフェースにデコードします。data
はポインタである必要があります。binary.Write(w io.Writer, order ByteOrder, data interface{}) error
:data
インターフェースの値を指定されたバイトオーダー (ByteOrder
) に従ってバイナリデータにエンコードし、io.Writer
に書き込みます。ByteOrder
: バイトオーダー(エンディアン)を指定するためのインターフェースです。binary.BigEndian
(ビッグエンディアン)とbinary.LittleEndian
(リトルエンディアン)の2つの実装が提供されています。- サポートされる型: プリミティブな固定長整数型(
int8
,uint8
,int16
,uint16
など)、浮動小数点型(float32
,float64
)、複素数型(complex64
,complex128
)、これらの型の配列やスライス、そしてこれらの型のみを含む構造体です。
reflect
パッケージ
reflect
パッケージは、Goプログラムが実行時に自身の構造を検査し、操作するためのリフレクション機能を提供します。これにより、プログラムは型情報、フィールド、メソッドなどを動的に取得・操作できます。
reflect.ValueOf(i interface{}) Value
: インターフェース値i
のリフレクションValue
を返します。reflect.TypeOf(i interface{}) Type
: インターフェース値i
のリフレクションType
を返します。reflect.Value
: Goの値を表すリフレクションオブジェクトです。Kind()
,Type()
,NumField()
,Field(i)
などのメソッドを通じて、値の型、フィールド、要素などにアクセスできます。reflect.Type
: Goの型を表すリフレクションオブジェクトです。Kind()
,Elem()
,Size()
,String()
などのメソッドを通じて、型の種類、要素型、サイズ、文字列表現などにアクセスできます。
encoding/binary
パッケージは、内部で reflect
パッケージを使用して、与えられた data
インターフェースの型情報を動的に検査し、そのサイズを計算したり、各フィールドを処理したりします。
io.Reader
と io.Writer
Go言語における基本的なI/Oインターフェースです。
io.Reader
:Read(p []byte) (n int, err error)
メソッドを持つインターフェースで、データを読み込むための抽象化を提供します。io.Writer
:Write(p []byte) (n int, err error)
メソッドを持つインターフェースで、データを書き込むための抽象化を提供します。
エラーハンドリング
Go言語では、エラーは戻り値として明示的に返されます。このコミットでは、エラーをより詳細にすることで、デバッグ時の情報量を増やしています。
技術的詳細
このコミットの核心は、encoding/binary
パッケージが内部で使用する型サイズ計算関数 dataSize
と sizeof
のシグネチャ変更と、それに伴うエラー伝播の改善、そしてエラーメッセージの具体化です。
dataSize
と sizeof
関数の変更
変更前は、dataSize
と sizeof
関数は、無効な型が渡された場合に -1
を返していました。これは、エラーが発生したことを示すための慣習的な方法でしたが、具体的なエラー内容(どの型が問題なのか)を伝えることはできませんでした。
変更前:
func dataSize(v reflect.Value) int { ... return -1 }
func sizeof(t reflect.Type) int { ... return -1 }
変更後:
func dataSize(v reflect.Value) (int, error) { ... return 0, err }
func sizeof(t reflect.Type) (int, error) { ... return 0, errors.New("invalid type " + t.String()) }
この変更により、dataSize
と sizeof
は、計算されたサイズに加えて error
型の戻り値を返すようになりました。これにより、エラーが発生した場合に nil
ではないエラーオブジェクトを返し、そのエラーオブジェクトに具体的なエラーメッセージを含めることが可能になります。
特に sizeof
関数では、サポートされていない型が渡された場合に errors.New("invalid type " + t.String())
を返すようになりました。ここで t.String()
は、問題となっている reflect.Type
の文字列表現(例: string
, []int
, struct { ... }
など)を返します。これにより、エラーメッセージに具体的な型名が含まれるようになります。
エラー伝播の改善
dataSize
と sizeof
がエラーを返すようになったため、これらの関数を呼び出す上位の関数(Read
, Write
, Size
, dataSize
自身、sizeof
自身)も、返されたエラーを適切にチェックし、必要に応じてそのエラーをさらに上位に伝播させるように変更されました。
例えば、Read
関数や Write
関数では、dataSize(v)
の呼び出し結果を size, err := dataSize(v)
のように受け取り、if err != nil
でエラーをチェックし、return errors.New("binary.Read: " + err.Error())
のように、元のエラーメッセージにプレフィックスを付けて返しています。これにより、エラーの発生源と具体的な内容が明確になります。
エラーメッセージの具体化
この変更の最も重要な点は、ユーザーに表示されるエラーメッセージが改善されたことです。以前は「invalid type S」のように、問題のある構造体全体が示されていましたが、変更後は「invalid type string」や「invalid type []int」のように、構造体内のどのフィールドの型が問題なのか、あるいはどのスライス要素の型が問題なのかが明確に示されるようになりました。
これは、sizeof
関数が errors.New("invalid type " + t.String())
を返すようになったことと、そのエラーが適切に伝播されるようになったことによって実現されています。
テストコードの変更
binary_test.go
の TestWriteT
関数も、この変更に合わせて更新されました。以前はエラーが nil
でないことだけを確認していましたが、変更後は strings.Contains(err.Error(), typ)
を使用して、エラーメッセージが期待する型情報を含んでいるかどうかを検証するようになりました。これにより、エラーメッセージの具体化が正しく機能していることを保証しています。
コアとなるコードの変更箇所
src/pkg/encoding/binary/binary.go
Read
関数
--- a/src/pkg/encoding/binary/binary.go
+++ b/src/pkg/encoding/binary/binary.go
@@ -167,9 +167,9 @@ func Read(r io.Reader, order ByteOrder, data interface{}) error {
default:
return errors.New("binary.Read: invalid type " + d.Type().String())
}
- size := dataSize(v)
- if size < 0 {
- return errors.New("binary.Read: invalid type " + v.Type().String())
+ size, err := dataSize(v)
+ if err != nil {
+ return errors.New("binary.Read: " + err.Error())
}
d := &decoder{order: order, buf: make([]byte, size)}
if _, err := io.ReadFull(r, d.buf); err != nil {
dataSize(v)
の戻り値が (int, error)
に変更されたため、size, err := dataSize(v)
で受け取り、err
が nil
でない場合にそのエラーをラップして返しています。
Write
関数
--- a/src/pkg/encoding/binary/binary.go
+++ b/src/pkg/encoding/binary/binary.go
@@ -247,64 +247,68 @@ func Write(w io.Writer, order ByteOrder, data interface{}) error {
// Fallback to reflect-based encoding.
v := reflect.Indirect(reflect.ValueOf(data))
- size := dataSize(v)
- if size < 0 {
- return errors.New("binary.Write: invalid type " + v.Type().String())
+ size, err := dataSize(v)
+ if err != nil {
+ return errors.New("binary.Write: " + err.Error())
}
buf := make([]byte, size)
e := &encoder{order: order, buf: buf}
e.value(v)
- _, err := w.Write(buf)
+ _, err = w.Write(buf)
return err
}
Read
関数と同様に、dataSize(v)
の戻り値の変更に対応し、エラーハンドリングを改善しています。
Size
関数
--- a/src/pkg/encoding/binary/binary.go
+++ b/src/pkg/encoding/binary/binary.go
@@ -259,9 +259,11 @@ func Write(w io.Writer, order ByteOrder, data interface{}) error {
// Size returns how many bytes Write would generate to encode the value v, which
// must be a fixed-size value or a slice of fixed-size values, or a pointer to such data.
func Size(v interface{}) int {
- return dataSize(reflect.Indirect(reflect.ValueOf(v)))
+ n, err := dataSize(reflect.Indirect(reflect.ValueOf(v)))
+ if err != nil {
+ return -1
+ }
+ return n
}
dataSize
がエラーを返すようになったため、エラーが発生した場合は以前と同様に -1
を返すように変更されています。
dataSize
関数
--- a/src/pkg/encoding/binary/binary.go
+++ b/src/pkg/encoding/binary/binary.go
@@ -269,29 +271,31 @@ func Size(v interface{}) int {
// dataSize returns the number of bytes the actual data represented by v occupies in memory.
// For compound structures, it sums the sizes of the elements. Thus, for instance, for a slice
// it returns the length of the slice times the element size and does not count the memory
// occupied by the header.
-func dataSize(v reflect.Value) int {
+func dataSize(v reflect.Value) (int, error) {
if v.Kind() == reflect.Slice {
- elem := sizeof(v.Type().Elem())
- if elem < 0 {
- return -1
+ elem, err := sizeof(v.Type().Elem())
+ if err != nil {
+ return 0, err
}
- return v.Len() * elem
+ return v.Len() * elem, nil
}
return sizeof(v.Type())
}
dataSize
自身も sizeof
の戻り値の変更に対応し、エラーを伝播させるように変更されました。
sizeof
関数
--- a/src/pkg/encoding/binary/binary.go
+++ b/src/pkg/encoding/binary/binary.go
@@ -301,29 +305,31 @@ func dataSize(v reflect.Value) (int, error) {
}
-func sizeof(t reflect.Type) int {
+func sizeof(t reflect.Type) (int, error) {
switch t.Kind() {
case reflect.Array:
- n := sizeof(t.Elem())
- if n < 0 {
- return -1
+ n, err := sizeof(t.Elem())
+ if err != nil {
+ return 0, err
}
- return t.Len() * n
+ return t.Len() * n, nil
case reflect.Struct:
sum := 0
for i, n := 0, t.NumField(); i < n; i++ {
- s := sizeof(t.Field(i).Type)
- if s < 0 {
- return -1
+ s, err := sizeof(t.Field(i).Type)
+ if err != nil {
+ return 0, err
}
sum += s
}
- return sum
+ return sum, nil
case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64,
reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
reflect.Float32, reflect.Float64, reflect.Complex64, reflect.Complex128:
- return int(t.Size())
+ return int(t.Size()), nil
}
- return -1
+ return 0, errors.New("invalid type " + t.String())
}
sizeof
関数が (int, error)
を返すように変更されました。reflect.Array
や reflect.Struct
のケースでは、再帰的に sizeof
を呼び出し、エラーを伝播させます。サポートされていない型の場合(default
ケース)は、errors.New("invalid type " + t.String())
を返して、具体的な型名をエラーメッセージに含めるようになりました。
skip
メソッド (decoder, encoder)
--- a/src/pkg/encoding/binary/binary.go
+++ b/src/pkg/encoding/binary/binary.go
@@ -514,11 +518,12 @@ func (e *encoder) value(v reflect.Value) {
}
func (d *decoder) skip(v reflect.Value) {
- d.buf = d.buf[dataSize(v):]
+ n, _ := dataSize(v)
+ d.buf = d.buf[n:]
}
func (e *encoder) skip(v reflect.Value) {
- n := dataSize(v)
+ n, _ := dataSize(v)
for i := range e.buf[0:n] {
e.buf[i] = 0
}
dataSize
の戻り値が変更されたため、エラーを無視してサイズのみを取得するように変更されています。これは、skip
の目的がバッファのポインタを進めることであり、型エラーが発生してもそのサイズ分だけ進める必要があるためです。
src/pkg/encoding/binary/binary_test.go
TestWriteT
関数
--- a/src/pkg/encoding/binary/binary_test.go
+++ b/src/pkg/encoding/binary/binary_test.go
@@ -9,6 +9,7 @@ import (
"io"
"math"
"reflect"
+ "strings"
"testing"
)
@@ -149,8 +150,14 @@ func TestWriteT(t *testing.T) {
tv := reflect.Indirect(reflect.ValueOf(ts))
for i, n := 0, tv.NumField(); i < n; i++ {
+ typ := tv.Field(i).Type().String()
+ if typ == "[4]int" {
+ typ = "int" // the problem is int, not the [4]
+ }
if err := Write(buf, BigEndian, tv.Field(i).Interface()); err == nil {
t.Errorf("WriteT.%v: have err == nil, want non-nil", tv.Field(i).Type())
+ } else if !strings.Contains(err.Error(), typ) {
+ t.Errorf("WriteT: have err == %q, want it to mention %s", err, typ)
}
}
}
strings
パッケージがインポートされ、エラーメッセージが期待する型情報を含んでいるかを strings.Contains
で確認するようになりました。これにより、エラーメッセージの具体化が正しく行われているかをテストしています。[4]int
のような配列型の場合、問題は要素の型 (int
) にあるため、エラーメッセージのチェックでは int
を期待するように調整されています。
BenchmarkReadStruct
関数
--- a/src/pkg/encoding/binary/binary_test.go
+++ b/src/pkg/encoding/binary/binary_test.go
@@ -238,7 +245,7 @@ func BenchmarkReadStruct(b *testing.B) {
bsr := &byteSliceReader{}
var buf bytes.Buffer
Write(&buf, BigEndian, &s)
- n := dataSize(reflect.ValueOf(s))
+ n, _ := dataSize(reflect.ValueOf(s))
b.SetBytes(int64(n))
t := s
b.ResetTimer()
dataSize
の戻り値が変更されたため、エラーを無視してサイズのみを取得するように変更されています。ベンチマークの目的はサイズを取得することであり、エラーハンドリングはここでは重要ではないためです。
コアとなるコードの解説
このコミットは、Go言語におけるエラーハンドリングのベストプラクティスとリフレクションの活用を示しています。
-
エラー情報の詳細化: 以前はエラーが発生した際に「無効な型」という一般的なメッセージしか提供されませんでしたが、この変更により、
sizeof
関数が直接errors.New("invalid type " + t.String())
を返すようになりました。これにより、エラーメッセージに問題のある具体的な型(例:string
,map
,chan
など)が含まれるようになり、開発者はエラーの原因を迅速に特定できるようになります。 -
エラーの伝播:
dataSize
やsizeof
のような下位レベルの関数で発生したエラーが、Read
やWrite
といった上位レベルの関数に適切に伝播されるようになりました。Goのエラーハンドリングの慣習に従い、エラーを戻り値として返し、呼び出し元でそのエラーをチェックし、必要に応じてラップして再スローすることで、エラーの発生源と経路を追跡しやすくなります。 -
リフレクションの活用:
encoding/binary
パッケージは、Goの型システムを動的に検査するためにreflect
パッケージを extensively に使用しています。この変更では、reflect.Type.String()
メソッドを活用して、実行時に取得した型情報をエラーメッセージに組み込むことで、より動的で有用なエラー報告を実現しています。 -
テストの堅牢化:
TestWriteT
の変更は、単にエラーが発生したかどうかだけでなく、エラーメッセージの内容まで検証することで、テストの堅牢性を高めています。これは、ユーザーに提供されるエラーメッセージの品質を保証するために非常に重要です。
このコミットは、Go言語のライブラリ開発において、ユーザーエクスペリエンス(特にデバッグ体験)を向上させるための細やかな配慮がなされていることを示しています。エラーメッセージの具体化は、開発者が問題を迅速に解決し、生産性を向上させる上で非常に役立ちます。
関連リンク
- Go Issue #4825: encoding/binary: make type error more specific
- Go CL 7301102: encoding/binary: make type error more specific
参考にした情報源リンク
- Go 言語公式ドキュメント:
encoding/binary
パッケージ - Go 言語公式ドキュメント:
reflect
パッケージ - Go 言語公式ドキュメント:
io
パッケージ - Go 言語におけるエラーハンドリング (一般的なGoのエラーハンドリングの概念)
- Go 言語におけるリフレクション (一般的なGoのリフレクションの概念)