6
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Goでinterfaceのポインタを使わない理由と使う場面

Posted at

はじめに

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.

TTのみ、型*TT/*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を想定すればよい

である。

ここで最も好ましいとされる方法である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がレシーバのメソッドを含まないようである。

  1. 言葉で書くと何言っているかわからないので、ソース見た方が早い。

  2. この問題に触れているのを見かけたことがないのが個人的には不思議で間違っていないか少し不安ではある。

  3. 記事は2009年のものでだいぶ古いので不安になるが、2021年現在の実装を軽く確認するかぎり、変わっていないよう。例えばinterfaceの定義はここで、method tableにinterface定義のメソッドのみ含めるのはこのあたり。型がword以下の場合にポインタではなく直接dataに該当データをいれる最適化のコードは小1時間探したが見つけることはできなかった。https://github.com/teh-cmc/go-internals/tree/master/chapter2_interfaces とか読めばわかるかもしれない。

6
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?