この前、IDでの検索を行う関数をGo言語で作成しました。以前使っていたJavaの癖で、指定したIDが存在しなかった場合は値にもエラーにもnilを返すように実装していたのですが、Go言語では良くない設計のようです。
自分なりになぜnilを返すのが良くないのか調査したので、その内容をまとめます。
なぜ何も見つからない場合に戻り値にnilだけを返すのが良くないのか
nilを返すと書いていますが、Go言語では複数の戻り値を返せるので、正確には戻り値が(value, err)であったメソッドで(nil, nil)を返していたということになります。
私が以前使っていたJavaでは、Optionalが実装されるJava8までだと何も見つからない場合はnullを返すことが一般的でした。なんとなくその頃の癖で(nil, nil)を返してしまっていたのです。
しかし、(nil, nil)を返す実装だと以下のような問題が生じます。
nil pointerを起こす可能性が高まる
何も見つからない場合にvalueとしてnilが返るのは仕方ないとしても、errorがnilだとnil pointerでpanicが起きる可能性が高まります。
大抵の場合、戻り値にerrorがある場合まずはerrorの有無を確認するはずです。
1 2 3 4 5 6 | // someFuncのシグネチャはfunc someFunc(id int) (value *Value, err error) value, err := someFunc( 100 ) if err != nil { // なにかエラー処理 } // なにかvalueへの処理 |
errorがnilでなければ戻り値に何らかの問題があることがはっきりします。
ところが(nil, nil)を返すとエラー処理の部分に入りません。そのため、明示的なnilチェックをしたいない場合nil pointerが発生することになります。
利用者に状況が伝わらない
Go言語でのerrorは、必ずしもエラーだけを意味するわけではなくて例外的な状況も表すようです。
指定したIDが見つからないことは例外的な状況であることが多いでしょう。にも関わらず(nil, nil)を返すのは、妥当ではないようです。
どのように関数やメソッドを設計するべきか
では、何も見つからない場合はどのように設計するべきなのでしょうか。
どうやら2パターンがGo言語ではよく使われているようです。
- 何も見つからない場合、それを意味する特定のerrorを返す
- 何も見つからない以外の問題が発生しない場合、なにか見つかったかどうかのフラグを返す
特定のエラーを返す
(nil, nil)を返すのが良くない以上、基本的には何も見つからない場合はerrorを返すのが良さそうです。
流行りのChatGPTで聞いてみたところ、こんなサンプルを返してきました。
1 2 3 4 5 6 7 | func searchID(id int ) ( string , error) { // code to search for the ID // ... // if the ID is not found, return a sentinel value and an error indicating that the ID was not found return "Not Found" , fmt.Errorf( "ID not found: %d" , id) } |
戻り値がポインタではなく文字列なので空文字列を返していますが、errorには毎回fmt.Errorfで作ったエラーを返す形の実装です。
ただGo言語の実装では、毎回fmt.Errorfなどでエラーを作成するのではなく、グローバルなエラーを定義することが多いみたいです。
例えばos.Openメソッドでは、指定したファイルが見つからない場合fs.ErrNotExistというエラーを返すようになっています。ファイルが見つからない場合とそうでない場合を切り分けて処理ができるわけです。
例えばこんな感じ。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | func main() { // 指定したファイルは存在しないので、fs.ErrNotExistがerrに返ってくる file, err := os.Open( "path/to/not/exists" ) if err != nil { if errors.Is(err, fs.ErrNotExist) { // したがって、ここの処理が実行される fmt. Println ( "ErrNotExist" ) return } fmt. Println (err) return } bytes, err := io.ReadAll(file) if err != nil { fmt. Println (err) return } fmt. Println (bytes) } |
ちなみにエラー判定で使っているerrors.Isはラップしたエラーも判定してくれます。あるエラーにコメントを追加したい場合、このような形でエラーを返せます。
1 2 3 4 | // この関数の戻り値はerrors.Is(err, fs.ErrNotExist)でtrueを返す func returnWrapedErr() error{ return fmt.Errorf( "additional error message : %w" , fs.ErrNotExist) } |
見つかったかどうかのフラグを返す
何かが見つかったかどうか以外で問題が発生し得ない場合、errorではなくboolでフラグを返すこともあります。
よく使いそうなのがmapでキーを指定する時でしょうか。関数やメソッドとは少し違いますが、意味合いは同じです。
1 2 3 4 5 6 7 8 | func main() { list := make ( map [ string ] string ) value, ok := list[ "target" ] if !ok { fmt. Println ( "no value" ) } fmt. Println (value) } |
指定したものが存在したらokはtrue、なかったらfalseになります。
フリーランスとして独立することを考えているなら、テックビズをお勧めしています。現在私も契約中です。具体的なおすすめ具合は姉妹サイト「TECHBIZ nut」の次の記事を御覧ください。
また、契約には至りませんでしたがMidworksも対応が丁寧でした。
コメント