皆さまゴールデンウィークはいかがお過ごしでしょうか。
GW前に投稿しようと下書きにちまちま書き溜めていた本記事ですが、スマホで誤ってゴミ箱ボタンを押してしまったがために一瞬で電子の藻屑と化してしまい泣きながら記事を書き直しています。
せめて削除時は確認ダイアログぐらい出るようにQiitaには改善してもらいたいものです。。
閑話休題。
Go言語で複数エラーハンドリングするためにいい方法ないかなーとネットの海を彷徨っていたところ、なかなかよさげな記事を見つけたので実例を交えて書き残していきたいと思います。
go1.6.2で検証
エラー処理の基本
Go言語にはtry~catch~finallyの例外処理は存在しません。
http://golang.jp/go_faq#exceptions
Go言語ではエラーを処理するためにerrorインタフェースが用意されています。
type error interface {
Error() string
}
これはfunc Error() string
(Errorという名前で引数がなくstringを返却する関数)を実装した型はすべてerrorインタフェースが実装されているものとみなすということです。(ダックタイピング)
Go言語のインタフェース実装についてはこちらの記事が詳しいです。
→ Go言語における埋め込みによるインタフェースの部分実装パターン
エラーハンドリング例
Go言語では複数の戻り値を返却できる特性を利用して、戻り値としてerrorインタフェースを返却することによりエラーハンドリングを実現しています。
なお、errorインタフェースは戻り値の最後に付与するのが暗黙のルールのようです。
Go言語に用意された多くの関数もエラー時にerrorインタフェースを返却しています。
ファイルを開くos.Open関数では2番目の戻り値にerrorインタフェースが返却されます。
func Open(name string) (*File, error)
f, err := os.Open("/tmp/hogehoge.txt")
if err != nil {
// エラー時の処理
log.Fatal(err)
}
// 成功時の処理
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("(*>△<)<ナーンナーンっっ")
}
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)
}
err (*>△<)<ナーンナーンっっ
errorインタフェースを実装した構造体を返却する
errorインタフェースを実装した型のインスタンスを返却してあげることによりエラー処理を実現できます。
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}
}
err (*>△<)<ナーンナーンっっ [code=19]
エラー用の型は型であればなんでもよいので、空の構造体でも、なんならintやstringなどのプリミティブ型でもOKです。
プリミティブ型の例→ https://play.golang.org/p/QzPvBfqjq7s
複数のエラーハンドリング
前置きが長くなってしまいましたが、本記事の本題はここからとなります。
例外処理がないので複数のエラーハンドリングはどうすればいいんだと悩みました。
例えばURLと保存ファイル名を引数にファイルをダウンロードし、ディスクに保存する処理があったとします。
上記の処理で発生しうるエラーは下記のようなものが考えられます。
- コネクション失敗等ネットワークのエラー
- 404エラー等Webサーバから返ってくるエラー
- ファイル書き込み失敗等ファイルIOのエラー
例外処理の存在する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")
}
}
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 (*>△<)<ナーンナーンっっ
この方法はエラーを種別を判別するだけでしたら一番シンプルなように思えます。
Goの標準パッケージでも採用されています。(sql.ErrNoRows
とか)
ただ個人的にはあらかじめインスタンスを定義しておくというのが若干引っかかります。
その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")
}
}
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)終わりだよ~