Yappli Tech Blog

株式会社ヤプリの開発メンバーによるブログです。最新の技術情報からチーム・働き方に関するテーマまで、日々の熱い想いを持って発信していきます。

なぜGoのerrorインターフェースはポインタレシーバで実装するべきなのか

サーバーサイドエンジニアの @shuymn です。

先日 Go の独自エラーについての記事を投稿しました。そこで error インターフェースを実装する時はポインタレシーバで実装しましょうという話をしました。そちらでは詳細は長くなるので割愛させていただきましたが、この記事では Go のインターフェースについて深掘りしつつ、前回の記事の主張を補完します。

インターフェースを満たす構造体を実装する方法

Go ではインターフェースを満たす構造体を実装する方法は 2 種類あります。

  1. 値レシーバでメソッドを実装する
  2. ポインタレシーバでメソッドを実装する

以下は error インターフェースの実装サンプルコードです。

type ErrFoo struct{}

func (e ErrFoo) Error() string {
    return "foo"
}

type ErrBar struct{}

func (e *ErrBar) Error() string {
    return "bar"
}

それぞれみていきましょう。

値レシーバでメソッドを実装する

値レシーバでメソッドを実装した場合、その構造体は値・ポインタどちらもインターフェースを満たします。

func ErrFooAsValue() error {
    return ErrFoo{} // 値返し
}

func ErrFooAsPointer() error {
    return &ErrFoo{} // ポインタ返し
}

Go Playground

ポインタレシーバでメソッドを実装する

しかしポインタレシーバでメソッドを実装すると、構造体はポインタの場合に限ってインターフェースを満たします。

func ErrBarAsValue() error {
    return ErrBar{} // ErrBar does not implement error
}

func ErrBarAsPointer() error {
    return &ErrBar{} // ポインタ返し
}

Go Playground

しかしインターフェースとしてではなく構造体をそのまま扱う場合は、値型の変数からメソッドを呼び出すことができます。つまり、構造体をインターフェース型として扱うときと、構造体そのままの(具体)型として扱うときで差分があるということです。

type ErrBar struct {}

func (e *ErrBar) Error() string {
    return "bar"
}

func ErrBarAsValue() ErrBar {
    return ErrBar{} // OK
}

func main() {
    bar := ErrBarAsValue()
    fmt.Println(bar.Error()) // bar
}

Go Playground

この記事中では、以下の行為を自動解決と呼ぶことにします*1

  • ポインタレシーバで実装されたメソッドを値型の変数から呼び出す
  • 値レシーバで実装されたメソッドをポインタ型の変数から呼び出し

それではさらに自動解決について深掘りしていきます。

そもそも自動解決とは

言葉で説明してもややこしいので、以下に自動解決をまとめた表と Go Playground を用意しました。

値レシーバメソッド呼び出し ポインタレシーバメソッド呼び出し
値型変数 ○(自動解決)
ポインタ型変数 ○(自動解決)
インターフェース型変数(実体は値) ×(未実装扱い)
インターフェース型変数(実体はポインタ) ○(自動解決)

Go Playground

表を見てインターフェースを値レシーバで実装したほうが値型・ポインタ型どちらとしてでも扱うことができるので便利だと考える方もいるかもしれません。

値レシーバで実装すると不便になるのはインターフェース型から具体型を取り出すときです。そもそも抽象から具象を取り出すのは良くないコードのにおいがしますし、遭遇する機会も多くはないでしょう。

しかし簡単に思いつく例外としてエラー処理があります。これは値レシーバで error インターフェースを実装してしまうと 2 通りの考慮が必要になります。

func doSomething() error {
    return ErrFoo{}
}

func main() {
    err := doSomething()
    if err != nil {
        var (
            fooErrVal ErrFoo
            fooErrPtr *ErrFoo
        )
        // 2通り考慮しないといけないし、処理もどうせ共通だけどどっちかにしか
        // 型変換後の値が入ってないので共通化も少しクセがある
        if errors.As(err, &fooErrVal) {
            fmt.Println("err is ErrFoo")
        } else if errors.As(err, &fooErrPtr) {
            fmt.Println("err is *ErrFoo")
        } else {
            fmt.Println("err is not ErrFoo or *ErrFoo")
        }
        return
    }
}

Go Playground

つまり自分が前回の記事で主張したかったことは「error インターフェースをポインタレシーバで実装することで、自動解決の仕様を逆手にとりエラー処理にバグが混入する原因の 1 つをケアできる」ということです。

おわりに

いかがでしたでしょうか。

メソッドの自動解決は Go を書いていてなんとなくそういう仕様があると経験ベースで理解していましたが、今回それを深掘ることで Go に対する理解が深まりました。経験を知識で裏付けすることによって自信を持って Go を書いたりコードレビューしたりできるようになり、今後も社内外問わずこういった活動を続けていこうというモチベーションにもなりました。

参考にした記事等を末尾に紹介させていただきます。ありがとうございました。

参考

*1:A Tour of Gode では methods and pointer indirection と記載されるものです。