はじめに
Goで「interfaceのポインタは使うことがない」のが定石だけど、errors.As
のテストケースをみていて有用な場合があるのではと思った。
interfaceのポインタ
https://golang.org/doc/faq#pointer_to_interface
をみると、
Almost never. Pointers to interface values arise only in rare, tricky situations involving disguising an interface value's type for delayed evaluation.
にあるように、interfaceのポインタを使う場面はほぼないと言っていいようだ。
続いて以下のように続く。
It is a common mistake to pass a pointer to an interface value to a function expecting an interface. The compiler will complain about this error but the situation can still be confusing, because sometimes a pointer is necessary to satisfy an interface. The insight is that although a pointer to a concrete type can satisfy an interface, with one exception a pointer to an interface can never satisfy an interface.
型T
はT
のみ、型*T
はT
/*T
の両方のレシーバのメソッドを含む。
つまり、型T
と型*T
でメソッドセットが異なり、 型*T
がレシーバであるメソッドを含むinterfaceを満たすには型T
のポインタが必ず必要ということである。
ただし、その唯一の例外が型T
がinterfaceの場合であり、したがって基本的にinterfaceのポインタが必要になることはない。
以下がその様子の確認である。
// Write defined as
// func (b *Buffer) Write(p []byte) (n int, err error) {..
var buf bytes.Buffer
// io.Copy(buf, os.Stdin) <- compile error
io.Copy(&buf, os.Stdin)
var writer io.Writer = &buf
// io.Copy(&writer, os.Stdin) <- compile error
io.Copy(writer, os.Stdin)
errors.Asで使う
唐突であるが、Goでのエラー処理をどう実装すべきか悩むことは多いが、個人的には
https://dave.cheney.net/paste/gocon-spring-2016.pdf
がわかりやすく、気に入っている。主題とずれるので詳しくは書かないがエッセンスとしては
- エラーの分岐処理にてどのエラーかを判断する方法として、悪い方法 -> 良い方法の順に以下
-
if strings.Contains(err.Error(), "not found")
のように文字列比較 -
if err == io.EOF
のようにio.EOF
等のSentinel errorと比較 - 独自エラー型を定義して、
switch err := err.(type)
のように比較 - interfaceにのみ依存し、
if te, ok := err.(temporary); ok && te.Temporary()
のように比較する - エラーに付加情報を与えてエラーを返す。
- 記事は古いが、現在では、Go1.13以降のerrorsの
Wrap
/UnWrap
を想定すればよい
- 記事は古いが、現在では、Go1.13以降のerrorsの
である。
ここで最も好ましいとされる方法であるinterfaceとしてエラーを公開し、クライアント側でwrapされたエラーをハンドリングすることを想定する。
Go1.13以前では、https://github.com/pkg/errors を使って、
if te,ok := errors.Cause(err).(temporary); ok && te.Temporary(){
のようにするようにしていたが、厳密には「Cause
は最も元となるエラーを返すので、複数回wrapしているときに意図せず取得できない」1問題がある(とおもっている2)。
Go1.13以降のerrorsではそもそも似たようなことはできないと勘違いしていたのだが、たまたまerrorsのテストコードを眺めて、interfaceのポインタを利用するとうまくできるということに気づいた。
テストコードを参考に実際の利用想定されやすいような例を作ると以下のようになる。
package main
import (
"errors"
"fmt"
"os"
)
func main() {
err := connectDatabase()
if err != nil {
var timeout interface{ Timeout() bool }
// targetはnon-nilなポインタで、かつその先はinterface型かerrorをimplementしてる型である必要がある。
if errors.As(err, &timeout) {
fmt.Printf("Got error: %v\nTimeout return %v\n", err, timeout.Timeout())
// 以下は確認用であり、普通のアプリケーションコードでは書かない
if pathErr, ok := timeout.(*os.PathError); ok {
fmt.Printf("%#v", *pathErr)
}
}
}
}
func connectDatabase() error {
err := loadConfig()
if err != nil {
return fmt.Errorf("failed to load database config :%w", err)
}
//..
return nil
}
func loadConfig() error {
_, err := os.Open("non-existing")
return err
}
// OUTPUT IS BELOW
// Got error: failed to load database config :open non-existing: no such file or directory
// Timeout return false
// os.PathError{Op:"open", Path:"non-existing", Err:0x2}
また、ソースをみればわかるように先述のCause
の問題は発生しない。
interfaceのポインタを使うべき数少ない正しい場面だと思う。
[おまけ]型TはTのみ、型*TはT/*Tの両方のレシーバのメソッドを含むのはなぜか
https://golang.org/doc/faq#different_method_sets
をみると納得である。
As the Go specification says, the method set of a type T consists of all methods with receiver type T, while that of the corresponding pointer type *T consists of all methods with receiver *T or T. That means the method set of *T includes that of T, but not the reverse.
This distinction arises because if an interface value contains a pointer *T, a method call can obtain a value by dereferencing the pointer, but if an interface value contains a value T, there is no safe way for a method call to obtain a pointer. (Doing so would allow a method to modify the contents of the value inside the interface, which is not permitted by the language specification.)
interfaceの構造はRuss Coxさんが
https://research.swtch.com/interfaces
に素晴らしい記事を書いてくれている3。
interfaceにポインタ以外を代入した場合、コピーを作成し、data
がポインタとしてそこを参照する。記事でいうと、一番最初の図の200
がそのデータに該当する。
このメモリ領域の上書きは言語仕様上禁止されているようである。さらに仮に上書きできたとしても、*T
をレシーバとするメソッドをcallしたクライアントから見ると(修正されるのはあくまでcopy先だから)修正が反映されていないので混乱を生み、好ましくない。
こういった理由から型T
は*T
がレシーバのメソッドを含まないようである。
-
この問題に触れているのを見かけたことがないのが個人的には不思議で間違っていないか少し不安ではある。 ↩
-
記事は2009年のものでだいぶ古いので不安になるが、2021年現在の実装を軽く確認するかぎり、変わっていないよう。例えばinterfaceの定義はここで、method tableにinterface定義のメソッドのみ含めるのはこのあたり。型がword以下の場合にポインタではなく直接
data
に該当データをいれる最適化のコードは小1時間探したが見つけることはできなかった。https://github.com/teh-cmc/go-internals/tree/master/chapter2_interfaces とか読めばわかるかもしれない。 ↩