なぜ Go の errors パッケージの errorString は構造体なのか?

本記事は以下のイベントの内容を参考にしています。

tenntenn.connpass.com


結論

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 ライクなコードの書き方を身につけたい

本記事のモチベーション

開発環境

> go version
go version go1.18.1 darwin/amd64

リポジトリ

github.com

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("") を比較すると不一致判定となる

参考