Go言語のエラーハンドリングについて

  • 82
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

皆さまゴールデンウィークはいかがお過ごしでしょうか。
GW前に投稿しようと下書きにちまちま書き溜めていた本記事ですが、スマホで誤ってゴミ箱ボタンを押してしまったがために一瞬で電子の藻屑と化してしまい泣きながら記事を書き直しています。
せめて削除時は確認ダイアログぐらい出るようにQiitaには改善してもらいたいものです。。

閑話休題。
Go言語で複数エラーハンドリングするためにいい方法ないかなーとネットの海を彷徨っていたところ、なかなかよさげな記事を見つけたので実例を交えて書き残していきたいと思います。

go1.6.2で検証

エラー処理の基本

Go言語にはtry~catch~finallyの例外処理は存在しません。
http://golang.jp/go_faq#exceptions

Go言語ではエラーを処理するためにerrorインタフェースが用意されています。

https://golang.org/pkg/builtin/#error

errorインタフェース
type error interface {
    Error() string
}

Go言語では複数の戻り値を返却できる特性を利用して、戻り値としてerrorインタフェースを返却することによりエラーハンドリングを実現しています。
なお、errorインタフェースは戻り値の最後に付与するのが暗黙のルールのようです。

エラーハンドリング例

Error handling and Go

Go言語に用意された多くの関数もエラー時にerrorインタフェースを返却しています。
ファイルを開くos.Open関数では2番目の戻り値にerrorインタフェースが返却されます。

os.Open
func Open(name string) (*File, error)
エラーハンドリングの基本
f, err := os.Open("/tmp/hogehoge.txt")
if err != nil {
    // エラー時の処理
    log.Fatal(err)
}
// 成功時の処理
result
open /tmp/hogehoge.txt: no such file or directory

上記の例ではファイルが存在しなかった場合等にエラーが発生します。
エラーの有無はerrがnilか否かで判定します。

任意のエラーの生成

自前で作成した関数などで任意のerrorインタフェースを生成したいときは以下の方法で実現できます。

errors.Newを使う

errorsパッケージのNew関数を使うことで簡単にできます。
https://golang.org/pkg/errors/

package main

import (
    "errors"
    "fmt"
    "os"
)

func main() {
    if err := doError(); err != nil {
        fmt.Println("err", err)
        os.Exit(1)
    }
    fmt.Println("(o・∇・o)終わりだよ~") // ここにはこない
}
func doError() error {
    return errors.New("(*>△<)<ナーンナーンっっ")
}
result
err (*>△<)<ナーンナーンっっ

fmt.Errorfを使う

fmtパッケージのErrorfを使うことによりフォーマットを指定したエラーを返却することが可能です。
https://golang.org/pkg/fmt/#Errorf

package main

import (
    "fmt"
    "os"
)

func main() {
    if err := doError(); err != nil {
        fmt.Println(err)
        os.Exit(1)
    }
    fmt.Println("(o・∇・o)終わりだよ~") // ここにはこない
}
func doError() error {
    msg := "(*>△<)<ナーンナーンっっ"
    return fmt.Errorf("err %s", msg)
}
result
err (*>△<)<ナーンナーンっっ

errorインタフェースを実装した構造体を返却する

errorインタフェースを実装(stringを返却するError関数を実装した構造体を定義)し、インスタンスを返却してあげることによりエラー処理を実現できます。
Go言語のインタフェース実装についてはこちらの記事が詳しいです。
Go言語における埋め込みによるインタフェースの部分実装パターン

package main

import (
    "fmt"
    "os"
)

// エラー処理用の構造体
type MyError struct {
    Msg string
    Code int
}
// MyError構造体にerrorインタフェースのError関数を実装
func (err *MyError) Error() string {
    return fmt.Sprintf("err %s [code=%d]", err.Msg, err.Code)
}

func main() {
    if err := doError(); err != nil {
        fmt.Println(err)
        os.Exit(1)
    }
    fmt.Println("(o・∇・o)終わりだよ~") // ここにはこない
}
func doError() error {
    return &MyError{Msg: "(*>△<)<ナーンナーンっっ", Code: 19}
}
result
err (*>△<)<ナーンナーンっっ [code=19]

複数のエラーハンドリング

前置きが長くなってしまいましたが、本記事の本題はここからとなります。
例外処理がないので複数のエラーハンドリングはどうすればいいんだと悩みました。

例えばURLと保存ファイル名を引数にファイルをダウンロードし、ディスクに保存する処理があったとします。
上記の処理で発生しうるエラーは下記のようなものが考えられます。

  • コネクション失敗等ネットワークのエラー
  • 404エラー等Webサーバから返ってくるエラー
  • ファイル書き込み失敗等ファイルIOのエラー

例外処理の存在するphpだと下記のように書けます。

phpでの例
<?php
class NetworkException extends Exception {};
class DownloadException extends Exception {};
class FileIOException extends Exception {};
try {
    $data = downloadToFile($url, $filename);
} catch (NetworkException $e) {
    // ネットワークエラーの場合
} catch (DownloadException $e) {
    // ダウンロードエラーの場合
} catch (FileIOException $e) {
    // ファイル書き込みエラーの場合
}

downloadToFile関数はエラー箇所によってNetworkException,DownloadException,FileIOException例外を投げてきます。
それぞれの例外をcatchすることにより、後続の処理を振り分けることが可能です。

それではGo言語で実現する場合どのようになるでしょうか。

よくない例

苦し紛れに思いついた方法ですが、error毎にそれぞれ別々のerrorインタフェースを返却する方法です。

package main

import (
    "fmt"
    "errors"
)

const (
    NICK_MOCHO = "もちょ"
    NICK_TEN   = "天"
    NICK_NANSU = "ナンス"
)

func main() {
    printError(doError(NICK_MOCHO))
    printError(doError(NICK_TEN))
    printError(doError(NICK_NANSU))
}

func doError(nick string) (error, error) {
    if nick == NICK_MOCHO {
        return errors.New("(o・∇・o)エラーだよ~"), nil
    }
    if nick == NICK_NANSU {
        return nil, errors.New("(*>△<)<ナーンナーンっっ")
    }
    return nil, nil
}

func printError(mocho error, nansu error) {
    if mocho != nil {
        fmt.Println("mocho error", mocho)
    } else if nansu != nil {
        fmt.Println("nansu error", nansu)
    } else {
        fmt.Println("no error")
    }
}
result
mocho error (o・∇・o)エラーだよ~
no error
nansu error (*>△<)<ナーンナーンっっ

doErrorは引数にNICK_MOCHOが与えられた場合mochoエラー、引数にNICK_NANSUが与えられた場合nansuエラーを返却します。
mochoとnansuを区別するために2つの戻り値でerrorを返却していますが、これは大変効率が悪いです。というかダサい…

その1:errorインスタンスを比較する

package main

import (
    "errors"
    "fmt"
)

const (
    NICK_MOCHO = "もちょ"
    NICK_TEN   = "天"
    NICK_NANSU = "ナンス"
)

// 各種errorインスタンスを生成しておく
var (
    ErrMocho = errors.New("(o・∇・o)エラーだよ~")
    ErrNansu = errors.New("(*>△<)<ナーンナーンっっ")
)

func main() {
    printError(doError(NICK_MOCHO))
    printError(doError(NICK_TEN))
    printError(doError(NICK_NANSU))
}

func doError(nick string) error {
    if nick == NICK_MOCHO {
        // ErrMochoインスタンスを返却
        return ErrMocho
    }
    if nick == NICK_NANSU {
        // ErrNansuインスタンスを返却
        return ErrNansu
    }
    return nil
}

func printError(err error) {
    if err != nil {
        if err == ErrMocho {
            // ErrMocho用のエラー処理
            fmt.Println("ErrMocho", err)
        } else if err == ErrNansu {
            // ErrNansu用のエラー処理
            fmt.Println("ErrNansu", err)
        } else {
            fmt.Println("その他のエラー", err)
        }
    } else {
        fmt.Println("no error")
    }
}
ErrMocho (o・∇・o)エラーだよ~
no error
ErrNansu (*>△<)<ナーンナーンっっ

この方法はエラーを種別を判別するだけでしたら一番シンプルなように思えます。
ただ個人的にはあらかじめインスタンスを定義しておくというのが若干引っかかります。

その2:errorの型を判別する

package main

import (
    "fmt"
)

const (
    NICK_MOCHO = "もちょ"
    NICK_TEN   = "天"
    NICK_NANSU = "ナンス"
)

/** エラー構造体 */

type MochoError struct {
    Msg string
}

func (e *MochoError) Error() string {
    return "(o・∇・o)エラーだよ~"
}

type NansuError struct {
    Msg  string
    Code int
}

func (e *NansuError) Error() string {
    return "(*>△<)<ナーンナーンっっ"
}

func main() {
    printError(doError(NICK_MOCHO))
    printError(doError(NICK_TEN))
    printError(doError(NICK_NANSU))
}

func doError(nick string) error {
    if nick == NICK_MOCHO {
        return &MochoError{Msg: nick}
    }
    if nick == NICK_NANSU {
        return &NansuError{Msg: nick, Code: 19}
    }
    return nil
}

func printError(err error) {
    if err != nil {
        // 型でエラーの種類を判別する
        switch e := err.(type) {
        case *MochoError:
            fmt.Println("MochoError", err, "Msg:", e.Msg)
        case *NansuError:
            fmt.Println("NansuError", err, "Msg:", e.Msg, "Code:", e.Code)
        default:
            fmt.Println("その他のエラー", err)
        }
        // if文の場合はこのように判定
        if nansu, ok := err.(*NansuError); ok {
            fmt.Println("NansuError[if]", err, "Msg:", nansu.Msg, "Code:", nansu.Code)
        }
    } else {
        fmt.Println("no error")
    }
}
result
MochoError (o・∇・o)エラーだよ~ Msg: もちょ
no error
NansuError (*>△<)<ナーンナーンっっ Msg: ナンス Code: 19
NansuError[if] (*>△<)<ナーンナーンっっ Msg: ナンス Code: 19

この方法は若干定義が面倒ですが、構造体に任意の値を定義したりすることができるので一番しっくりきました。

おまけ

URLと保存ファイル名を引数にファイルをダウンロードし、ディスクに保存する処理のエラーハンドリング実装例

package main

import (
    "fmt"
    "io/ioutil"
    "net/http"
    "os"
)

type DownloadError struct {
  StatusCode int
}
func (e *DownloadError) Error() string {
    return "httpd error!"
}

type SaveError struct {
  Filename, Message string
}
func (e *SaveError) Error() string {
    return e.Message
}

func main() {
  retry_count := 0
  for {
    err := DownloadToFile("http://example.com/hogehoge.html", "/tmp/hoge.html")
    if err == nil {
      fmt.Println("download success!")
      os.Exit(0)
    }
    // errインスタンスの型判別
    switch e := err.(type) {
      // ダウンロードエラー
      case *DownloadError:
        fmt.Printf("ダウンロードエラー %s [retry=%d, code=%d]\n", e.Error(), retry_count, e.StatusCode)
        retry_count++
        if retry_count > 3 {
          fmt.Printf("retry count over\n")
          os.Exit(1)
        }
      case *SaveError:
          fmt.Printf("保存エラー: %s [filename=%s]\n", e.Message, e.Filename)
          os.Exit(2)
      default:
        fmt.Printf("その他のエラー: %s\n", err)
        os.Exit(3)
    }
  }
}

func DownloadToFile(url, filename string) error {
  // ファイルダウンロード
  resp, err := http.Get(url)
  if err != nil {
    return err
  }
  defer resp.Body.Close()
  // 200以外のレスポンスはダウンロードエラーとする
  if resp.StatusCode != http.StatusOK {
    return &DownloadError{StatusCode: resp.StatusCode}
  }
  data, err := ioutil.ReadAll(resp.Body)
  if err != nil {
    return err
  }
  // ファイルへ保存
  err = ioutil.WriteFile(filename, data, os.ModePerm)
  if err != nil {
    return &SaveError{Message: err.Error(), Filename: filename}
  }
  return nil
}

参考サイト

お気楽 Go 言語プログラミング入門
golang で複数のエラーをハンドリングする方法
エラー・ハンドリングについて

(o・∇・o)終わりだよ~