本記事は以下のイベントの内容を参考にしています。
結論
Question
// https://cs.opensource.google/go/go/+/master:src/errors/errors.go;l=63 // errorString is a trivial implementation of error. type errorString struct { s string }
errors パッケージの構造体 errorString は string のフィールドを1つしか持たないので、新規で構造体を定義する必要はないのでは?
Answer
// これで良いのでは? (=> 結論として不適) type errorString string
- errorString を string で定義すると、errors.New("") で宣言されたエラーの変数どうしを比較する際に、文字列同士の比較となり同じ文字列の場合に同値となってしまう
- なぜ同じ文字列のエラーの変数の比較が同値になるとマズい?
- 別パッケージ同士で同じ文字列にて宣言されたエラーの変数を比較した際に同値判定となってしまう。別パッケージのエラーは意味的に異なるので不適。
以下詳細。
モチベーション
本記事から Go の標準パッケージのコードリーディングの記事が増えると思う。増やしたい。
コードリーディングのモチベーション
- 趣味で Go を書いていて書き方に不安を感じている
- 「Go に入っては Go に従え」ができてるか不安
- 不安解消のために Go の標準パッケージのコードを読んで Go ライクなコードの書き方を身につけたい
本記事のモチベーション
- コードリーディングをしよう! | tenntenn Conference 2022を見て、これからコードリーディングを自走できるようになりたい
- 動画を見て分からなかった所を理解する
開発環境
> go version go version go1.18.1 darwin/amd64
リポジトリ
Question 詳細
コードリーディングをしよう! | tenntenn Conference 2022 で、errors パッケージの errorString はなぜ構造体?という話題が出た。
// https://cs.opensource.google/go/go/+/master:src/errors/errors.go;l=63 // errorString is a trivial implementation of error. type errorString struct { s string }
errors パッケージの構造体 errorString は string のフィールドを1つしか持たないので、新規で構造体を定義する必要はないのでは?
type errorString string
これで良いのではないか。
なぜ errorString は構造体なのか?
Answer 詳細
テストを見ると、「Different allocations should not be equal.」(異なる割り当ては等しくてはなりません。)というコメントがある。
// https://cs.opensource.google/go/go/+/master:src/errors/errors_test.go func TestNewEqual(t *testing.T) { // Different allocations should not be equal. if errors.New("abc") == errors.New("abc") { t.Errorf(`New("abc") == New("abc")`) } if errors.New("abc") == errors.New("xyz") { t.Errorf(`New("abc") == New("xyz")`) } ... }
逆に、異なる割り当てが等しいとどういう問題があるか?というと、パッケージ別で同じ文字列でエラーの変数を宣言した場合に、別パッケージのエラーが他のエラーと同値になってしまう問題がある。
errorString は errors.New("") で返される error インタフェースの実装である。
// https://cs.opensource.google/go/go/+/master:src/errors/errors.go;l=58 func New(text string) error { return &errorString{text} }
この errors.New("") で宣言されたエラーの変数どうしを比較する際に、errorString が文字列であると文字列同士の比較になってしまい、同じ文字列の場合に同値となってしまう。同じ文字列の場合に同値となると、パッケージ別で同じ文字列でエラーを定義した場合に同値となってしまう。別パッケージのエラーは意味的に異なるので不適。
なぜ &errorString{text}
の比較でフィールドが同じ場合に不一致判定になるのか?
ここが分からなかった。(今までの話は動画を見れば理解できる)
errors.New("") で宣言されたエラーの変数の実体は &errorString{}
となる。
// https://cs.opensource.google/go/go/+/master:src/errors/errors.go;l=58 func New(text string) error { return &errorString{text} }
この実体同士の比較では、フィールドの値が同じでも不一致判定となる。
package main import "fmt" type human struct { name string } func main() { h1 := &human{ name: "hoge", } h2 := &human{ name: "hoge", } fmt.Println(h1.name == h2.name) // true fmt.Println(h1 == h2) // false }
なぜ h1 と h2 の比較は false になるのか。
&
(アンパサンド)
package main import "fmt" type human struct { name string } func main() { h1 := &human{ name: "hoge", } h2 := &human{ name: "hoge", } fmt.Println(h1.name == h2.name) // true fmt.Println(h1 == h2) // false h3 := human{ name: "hoge", } h4 := human{ name: "hoge", } fmt.Println(h3.name == h4.name) // true fmt.Println(h3 == h4) // true }
アンパサンドを付与して初期化した同フィールドを持つ構造体は不一致判定、付与せずに初期化した同フィールドを持つ構造体は一致判定となる。
&
オペレータは、そのオペランド( operand )へのポインタを引き出します。
https://go-tour-jp.appspot.com/moretypes/1
アンパサンドを付与することでポインタ型となり、ポインタ型の比較となり、値は同じだがポインタは異なるので不一致判定となる。
まとめ
- errors.New("") で宣言されたエラーの変数は errorString のポインタ型
- 値ではなくポインタ型の比較となるため、同じフィールドを持つ errors.News("") を比較すると不一致判定となる