2022/02/06

[golang] errorをどうするとよいのか

処理をしていて期待にそぐわないことになるならエラーを返すだろう。
golang もそうだ。

 

golang には error 型というものがある。
「プログラミング言語Go」の p.226 も error インターフェースの節が振られている。

type error interface {
	Error() string
}

あれ、string 型なのが決まってるし、それ以外の型はないのかい??

使い方としては、 errors を importしておいて、 errors.New("もじれつ") でオブジェクトというかインスタンスというかを作るようだ。


文字列というのは非常に扱いづらいと思うのだ。
特にエラーとなると、それを受け取った方がエラーの種類に応じて動作を変えたいだろう。
その判定が文字列だとすると、エラーの内容を親切にしただけで今までのプログラムが動かなくなる可能性もある。
数字であれば、エラー値を変更しなければ判定文は動作するだろう。

 

よく見るのは、戻り値をタプルにして 2番目の戻り値が非nilだったらエラー、というパターンだ。 OK か NG かだけだたらタプルじゃなくて普通に結果を返すだけだろうが、まあそんな感じだ。
しかし、エラーハンドリングを文字列でやるというのもなんだか怖い。さっきも書いたが文字列の内容を書き換えただけで困ることになるからだ。

まあ、エラー処理についてはただ 1つだけの正解というものはなくて、エラー戦略をどうするかだと思っている。
じゃあその戦略をどうするかっていうことになるのだけど、そこは言語の得意・不得意もあるし、アプリの種類にもよるしで決定しづらい。


Go言語は try-catch がないので大局としては C言語と同じようなことになるんじゃなかろうか。
C言語では戻り値が1つしかないので、0を戻すと正常、trueを戻すと正常、NULL以外だと正常、などといろいろなパターンがあり、言語として決められたルールはなかった。
だいたい、0 が「偽」として扱われるのに 0 が正常というのはときどき扱いが悪かったのだけど、標準ライブラリはそうなっていることが多いので悩んだものだ。

 

よく Go言語の戻り値などで使用されている error はこちら。

https://go.dev/ref/spec#Errors

ただ、これは明示的な本体があるものではなく、 Error() という string を返す関数を持つという interface が用意されているだけだ。冒頭で書いたやつですな。

チュートリアルでは errors.New() で error を作っている例が出ていた。

Return and handle an error - The Go Programming Language
https://go.dev/doc/tutorial/handle-errors

固定文字列ならこれでよいのだけど、文字列を作りたい場合は fmt.Errorf() を使うとよい。

自分の error を作りたいなら、適当な構造体を用意して Error() を持たせるとよさそうだ。

01: package main
02: 
03: import (
04:     "errors"
05:     "fmt"
06: )
07: 
08: type MyError struct {}
09: func (e *MyError) Error() string {
10:     return "my error"
11: }
12: 
13: func func1() error {
14:     return errors.New("my error")
15: }
16: 
17: func func2() error {
18:     return &MyError{}
19: }
20: 
21: func main() {
22:     err1 := func1()
23:     err2 := func2()
24: 
25:     if err1 == err2 {
26:         fmt.Printf("same!\n")
27:     } else {
28:         fmt.Printf("not same!!\n")
29:     }
30: }

MyError 型を作った。
Error() で返す文字列は同じでもオブジェクトが異なるので else のルートを通る。
そりゃそうだ。

 

では文字列で比較するしかないかというとそうではなく、型アサーションでチェックするというやり方があるそうだ。
下の例ではどちらも Error() があるので error 型であるというのは一致するのだが、MyError 型かどうかで不一致になる。

01: package main
02: 
03: import (
04:     "errors"
05:     "fmt"
06: )
07: 
08: type MyError struct {}
09: func (e *MyError) Error() string {
10:     return "my error"
11: }
12: 
13: func func1() error {
14:     return errors.New("my error")
15: }
16: 
17: func func2() error {
18:     return &MyError{}
19: }
20: 
21: func main() {
22:     err1 := func1()
23:     err2 := func2()
24: 
25:     if _, ok := err1.(error); ok {
26:         fmt.Printf("err1 is 'error'\n")
27:     } else {
28:         fmt.Printf("err1 is not 'error'\n")
29:     }
30:     if _, ok := err1.(*MyError); ok {
31:         fmt.Printf("err1 is 'MyError'\n")
32:     } else {
33:         fmt.Printf("err1 is not 'MyError'\n")
34:     }
35: 
36:     if _, ok := err2.(error); ok {
37:         fmt.Printf("err2 is 'error'\n")
38:     } else {
39:         fmt.Printf("err2 is not 'error'\n")
40:     }
41:     if _, ok := err2.(*MyError); ok {
42:         fmt.Printf("err2 is 'MyError'\n")
43:     } else {
44:         fmt.Printf("err2 is not 'MyError'\n")
45:     }
46: }

err1 is 'error'
err1 is not 'MyError'
err2 is 'error'
err2 is 'MyError'

 

標準パッケージの os はエラーの種類が多そうだけど、それぞれ type を定義しているのだろうか?

https://cs.opensource.google/go/go/+/refs/tags/go1.17.6:src/os/error.go

固定のエラーは var でやっているようだ。
ErrInvalidfs.ErrInvalid で、fs.ErrInvalid は errInvalid()oserror.ErrInvalid を return していて、最終的には errors.New() になっている。遠い。
このタイプは type が定義されていないが var で与えられているから os.ErrInvalid と比較すればよいだろう。
型アサーションを使っての比較は fmt.Errorf() を使いたいようなエラーの場合だけだろうか?

エラーが比較するタイプなのか型アサーションでチェックするのかは実装を見ないと分からない。
まあ説明文はあるかもしれないが、あんまり実装側が考えたいところではないだろう。
osパッケージの場合にはヘルパー関数が用意されている。

他にもあるのかもしれんが同じページで Is の関数はこれらだった。
IsTimeout() 以外は underlyingError() が呼ばれていて、switch文で型アサーションをしていた。そんな書き方ができるのか。型switch文というものらしい。

01: package main
02: 
03: import (
04:     "errors"
05:     "fmt"
06: )
07: 
08: type MyError struct {}
09: func (e *MyError) Error() string {
10:     return "my error"
11: }
12: 
13: func check(target interface{}) string {
14:     switch target.(type) {
15:     case *MyError:
16:         return "MyError"
17:     case error:
18:         return "error"
19:     default:
20:         return "unknown"
21:     }
22: }
23: 
24: func func1() error {
25:     return errors.New("my error")
26: }
27: 
28: func func2() error {
29:     return &MyError{}
30: }
31: 
32: func main() {
33:     err1 := func1()
34:     err2 := func2()
35: 
36:     fmt.Printf("err1 is '%s'\n", check(err1))
37:     fmt.Printf("err2 is '%s'\n", check(err2))
38: }

err1 is 'error'
err2 is 'MyError'

便利だね。


で、ここからは私の都合だ。

gomobile というもので Android のライブラリを作っているのだが、制約がいろいろある。
書いてある場所を探せなかったのだが、引数や戻り値で使用できる型がある程度決まっているし、戻り値は 1つか2つ、2つの場合でも 2番目は error のみとなっている。
error を返した場合は Android側でも exception として扱ってくれるようだ。

で、だ。
Android 側に行ってしまうともう型アサーションなんかは使えないし、もちろんオブジェクトの比較もできないだろう。
となると、やっぱり文字列で判定するしかないという状況なのだ。

見つけた。 gobind の方だった。

Type restrictions
https://pkg.go.dev/golang.org/x/mobile/cmd/gobind#hdr-Type_restrictions

error がどういう扱いになるかは書かれていなかった。
じゃあもう文字列で考えるしかないだろう。

 

うん、この考察は一般には関係ないことだった。
異なるプラットフォームを行き来する場合には便利な機能が使えないこともあるよ、ということで。