tweeeetyのぶろぐ的めも

アウトプットが少なかったダメな自分をアウトプット<br>\(^o^)/

【go】golangのエラー処理メモ - ②. 例外はないがエラーハンドリングはできるよ(インスタンスや型でハンドリング)

はじめに

goをさわって数ヶ月ですが、雰囲気では書けていたものの
errorやエラーハンドリングについてはもやもやしたままだったので自分理解メモの②

関連

この記事の関連です。

アジェンダ

  1. goのエラーについて
  2. 一番簡単なエラーハンドリング
  3. エラーハンドリングのパターン
  4. パターン1: errorの文字列で
  5. パターン2: errors.Newのインスタンス
  6. パターン3: カスタムエラーのインスタンス
  7. パターン4: カスタムエラーの型で

1. goのエラーについて

goのエラーについては以下に記載してみました。
【go】golangのエラー処理メモ - ①. errorとError型とカスタムErrorと

あえて項目にするまでもなかったけど前提として載せておきます

2. 一番簡単なエラーハンドリング

goは例外というモノがなく、
基本的には関数を呼び出した結果がエラーな場合はその場で処理します。

簡単なエラーハンドリング

その場で処理しちゃう例の一番簡単なものです。

サンプル

サンプルコード

package main

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

func doSomething() error {
  return errors.New("doSomething is error.")
}

func main() {

  err := doSomething()

  // nilと比較してerrorオブジェクトが返ってたらエラーと見なす
  if err != nil {
    fmt.Println("main is failed")
    os.Exit(1)
  }

  fmt.Println("main is success")
}
出力
$ go run src/go-error-handling/sample01/main.go 
main is failed
exit status 1

エラーを無視する

また、エラーでも問題ない場合は_や単に何も受け取らないことで無視する事もできます。

サンプル

サンプルコード

package main

import (
  "errors"
  "fmt"
)

func doSomething() error {
  return errors.New("doSomething is error.")
}

func doHoge() {
  err := doSomething()

  // nilと比較してerrorオブジェクトが返ってたらエラーと見なす
  if err != nil {
    fmt.Println("doHoge is failed")
    return
  }

  // 処理を続ける
  fmt.Println("doHoge is success")
}

func doFuga() {

  // _ でerrorを無視する
  _ = doSomething()

  // 処理を続ける
  fmt.Println("doFuga is success")
}

func doPiyo() {
  // 単に受け取らないことも
  doSomething()

  // 処理を続ける
  fmt.Println("doPiyo is success")
}

func main() {

  doHoge()

  doFuga()

  doPiyo()

  fmt.Println("main is success")
}
出力
$ go run src/go-error-handling/sample02/main.go 
doHoge is failed
doFuga is success
doPiyo is success
main is success

3. エラーハンドリングのパターン

上記のような簡単なエラーハンドリングでも事足りる事はあります。

しかし、
実際は下位のmethodで発生したエラーをハンドリングしては上位に返して…
を繰り返すと困る事がでてきます。

自分は以下のような事が困りました。

  • 最上位でハンドリングで出力したものの根本原因がどこかわからない
  • エラーによって分岐したいがどのようにするのがベストプラクティスかわからない

エラーハンドリングのパーターンとしては以下のサイトが非常に参考になります。

参考サイトを参考に、
エラーハンドリングは以下のようなパターンで行う事ができます。

エラーハンドリングのパターン
  • パターン1: errorの文字列で
  • パターン2: errors.Newのインスタンス
  • パターン3: カスタムエラーのインスタンス
  • パターン4: カスタムエラーの型で

バッドノウハウなパターンも含まれてますが、
さっと書く使い捨てのscriptだったりではバッドノウハウパターンでも無いよりは良いかな、という印象です。

4. パターン1: errorの文字列で

errors.Newなりfmt.Errorfの文字列を受け取り側で比較するというものです。

単純ですがバッドノウハウとされているパターンです。

サンプル

サンプルコード

package main

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

const (
  ERROR_MSG_01 = "doSomething is error. b is true"
  ERROR_MSG_02 = "doSomething is error. b is false"
)

func doSomething(b bool) error {

  if b {
    return errors.New(ERROR_MSG_01)

  } else {
    return errors.New(ERROR_MSG_02)
  }

  return nil
}

func main() {

  err := doSomething(true)

  if fmt.Sprintf("%s", err) == ERROR_MSG_01 {
    fmt.Println("ERROR: ERROR_MSG_01に応じた処理を行う")
    os.Exit(1)

  } else if fmt.Sprintf("%s", err) == ERROR_MSG_02 {
    fmt.Println("ERROR: ERROR_MSG_02に応じた処理を行う")
    os.Exit(1)
  }
  fmt.Println("main is success")
}
出力
$ go run src/go-error-handling/sample03/main.go 
ERROR: ERROR_MSG_01に応じた処理を行う
exit status 1
困る点

ハンドリング後、さらに呼び出し元に返そうとするととたんに困ります…

  // fmt.Errorfでさらに呼び出し元に返すとハンドリングできなく...
  if fmt.Sprintf("%s", err) == ERROR_MSG_01 {
    return fmt.Errorf("ERROR: err=%+v", err)
  }

5. パターン2: errors.Newのインスタンス

errors.Newのインスタンスで比較するパターンです。

osパッケージなんかでも使われてたりします
https://golang.org/src/os/error.go#L11

サンプル

サンプルコード

package main

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

const (
  ERROR_MSG_01 = "doSomething is error. b is true"
  ERROR_MSG_02 = "doSomething is error. b is false"
)

var (
  ERROR_01 = errors.New(ERROR_MSG_01)
  ERROR_02 = errors.New(ERROR_MSG_02)
)

func doSomething(b bool) error {

  if b {
    return ERROR_01

  } else {
    return ERROR_02
  }

  return nil
}

func main() {

  err := doSomething(false)

  if err == ERROR_01 {
    fmt.Println("ERROR: インスタンスERROR_01に応じた処理を行う")
    os.Exit(1)

  } else if err == ERROR_02 {
    fmt.Println("ERROR: インスタンスERROR_02に応じた処理を行う")
    os.Exit(1)
  }
  fmt.Println("main is success")
}

コード自体はだいぶマシになった気はしてきます

出力
$ go run src/go-error-handling/sample04/main.go 
ERROR: インスタンスERROR_02に応じた処理を行う
exit status 1
困る点

パターン1と同様ですがハンドリングの結果、上位にエラーを返すときに困ります。

そのまま返すとどこでエラーだったのかわかりずらくなり、
fmt.Errorfなどで新規にインスタンスを作ってしまうと別物になってしまいます。

  // そのまま返し続けると結局どこのエラーだったのか...という感じに
  if err == ERROR_01 {
    return ERROR_01
  }
  // fmt.Errorfをかますと、違うインスタンスになってしまう
  if err == ERROR_01 {
    return fmt.Errorf("ERROR: err=%+v", ERROR_01)
  } 

6. パターン3: カスタムエラーのインスタンス

【go】golangのエラー処理メモ - ①. errorとError型とカスタムErrorと
でも触れたカスタムエラーを定義してそのインスタンスでハンドリングするパターンです。
1つのカスタムエラーを使い回してますが、カスタムエラー自体を複数用意しても良いです。

サンプル

サンプルコード

package main

import "fmt"

const (
  ERROR_MSG_01 = "doSomething is error. b is true"
  ERROR_MSG_02 = "doSomething is error. b is false"
)

type MyError struct {
  Msg  string
  Code int
}

func (err *MyError) Error() string {
  return fmt.Sprintf("%s, %d", err.Msg, err.Code)
}

var (
  MyError_01 = &MyError{Msg: "MyError_001 is occur", Code: 30001}
  MyError_02 = &MyError{Msg: "MyError_002 is occur", Code: 30002}
)

func doSomething(b bool) error {

  if b {
    return MyError_01

  } else {
    return MyError_02
  }

  return nil
}

func main() {

  err := doSomething(false)

  switch err {
  case MyError_01:
    fmt.Println("ERROR: インスタンスMyError_01に応じた処理")
    fmt.Printf("ERROR: %+v\n", err)

  case MyError_02:
    fmt.Println("ERROR: インスタンスMyError_02に応じた処理")
    fmt.Printf("ERROR: %+v\n", err)

    // ちなみにdoSomethingの返り値は
    // errorインターフェースであってMyError_01ではない。
    // ココに以下のようなコードは書けない
    //fmt.Printf("ERROR: Msg=%s, Code=%d", err.Msg, err.Code)
  }
  fmt.Println("main is success")
}
出力
$ go run src/go-error-handling/sample05/main.go 
ERROR: インスタンスMyError_02に応じた処理
ERROR: MyError_002 is occur, 30002
main is success
困る点

パターン2に同じ

7. パターン4: カスタムエラーの型で

カスタムエラーの型でハンドリングするパターンです。

err.(type)という記述をしますが、
これはerrorがinterface型のために行えるらしいです。(知らなかった)

また、これをConversion構文というらしいです。
https://golang.org/ref/spec#Conversions

サンプル

サンプルコード

package main

import "fmt"

type MyError_01 struct {
  Code int
}

func (err *MyError_01) Error() string {
  return fmt.Sprintf("this is MyError_01")
}

type MyError_02 struct {
  Code int
}

func (err *MyError_02) Error() string {
  return fmt.Sprintf("this is MyError_02")
}

func doSomething(b bool) error {

  if b {
    return &MyError_01{Code: 30001}

  } else {
    return &MyError_02{Code: 30002}
  }

  return nil
}

func main() {

  err := doSomething(false)

  switch e := err.(type) {
  case *MyError_01:
    fmt.Println("ERROR: MyError_01の型に応じた処理")
    fmt.Printf("ERROR: err=%+v, e=%+v, code=%d\n", err, e, e.Code)

  case *MyError_02:
    //fmt.Printf("ERROR: MyError_02のエラー, err=%+v, code=%d\n", e, e.Code)
    fmt.Println("ERROR: MyError_02の型に応じた処理")
    fmt.Printf("ERROR: err=%+v, e=%+v, code=%d\n", err, e, e.Code)
  }

  fmt.Println("main is success")
}
出力
$ go run src/go-error-handling/sample06/main.go 
ERROR: MyError_02の型に応じた処理
ERROR: err=this is MyError_02, e=this is MyError_02, code=30002
main is success
困る点

これまでの中では一番すっきりした感があります。

しかし、参考サイトまんまの受け売りですが、
このままerrorを上に上に伝搬させていくと
上位ロジックが全てのerrorを把握している必要がある、という感じ。

ではどうするのか

pkg/errorsを使うと良いらしい
が、長くなったのでまた次の記事にします。

これもまた参考サイトが非常に参考になりました!

おわり

errorについても奥が深い!\(^o^)/