Go

Go言語の基本文法6(エラー)

こんにちは、イサムです!

本記事では、前回に引き続きGo言語の基本文法(エラー)についてまとめます。

対象読者
  • Go言語の基本文法について知りたい!
  • Go言語のエラーハンドリングについて知りたい!
この記事でやること
  • Go言語の基本文法(エラー)についてのまとめ

エラー

プログラムには、正常系と異常系があります。
正常系とは、プログラムが意図した仕様通りの動作することで、異常系とは、プログラムが意図しない動作をし、実行中のプログラムが中断、終了することです。
この異常系がエラーになります。
エラーの原因は、ソースコードの記述の誤り(バグ)がほとんどです。エラーが発生しない完璧なプログラムを開発するのは、不可能なため、意図しないエラーが発生した時にプログラムが中断、終了しないようにエラーハンドリング(エラー処理)をする必要があります。
そこで、Go言語では、エラーハンドリングするためのerrorsパッケージが標準で備わっているため、これを使用することでプログラムが意図しない動作をしても、プログラムが中断、終了することを回避することができます。

エラーハンドリングで重要なこと

  1. 必要十分な正しい情報を伝えること
    • どの処理の時に、エラーが発生したのか
    • エラーの持つ情報でエラーの原因を追える
    • デバッグ用などの無駄な情報は残さない(本番環境)
  2. 受け取り手によって伝え方を変える
    • プログラムの開発メンバー向けの内容なのか
    • プログラムの保守運用メンバー向けの内容なのか
    • プログラムを使用するエンドユーザー向けの内容なのか

エラーハンドリングの基本

Go言語には、try~catch~finallyの例外処理は存在しません。
Go言語では、エラーハンドリングするために下記のerrorインターフェースが用意されています。

type error interface {
  Error() string
}

これは、引数がないstringのデータを返すError関数を実装した型はすべてerrorインタフェースが実装されているものとみなします。
(インターフェースについては、こちらの記事を参考にしてみてください。)

ユーザー型にerrorインターフェースを実装させ、ある関数でそのユーザー型を返すと、Error関数で定義した文字列を返すことができます。

import (
  "fmt"
  "time"
)

// エラー時に取得するデータの構造体を定義
type MyError struct {
  time time.Time
  message string
}

// 構造体にerrorインターフェースを実装
func (e *MyError) Error() string {
  return fmt.Sprintf("at %v, %s", e.time, e.message)
}

func main() {
  if err := errRun(); err != nil {
    fmt.Println(err)
  }
}

// エラーを返す関数
func errRun() error {
  return &MyError{time: time.Now(), message: "it didn't work"}
}

結果

at 2022-03-11 13:45:56 +0000 UTC m=+0.000000001, it didn't work

エラーハンドリングの例

Go言語では、複数の戻り値を返す特性を利用し、戻り値としてerrorインターフェースを返すことでエラーハンドリングを実現します。
errorインターフェースの戻り値は最後に設定するのが慣例となっています。

Go言語に用意されている多くの関数もエラー時にerrorインターフェースを返しています。
例えば、ファイルを開くosパッケージのOpen関数では、2番目の戻り値にerrorインターフェースを返しています。

func Open(name string) (*File, error)

このOpen関数を使用し、指定したファイルを開こうとするが、そのファイルが存在しなかった場合、エラーを返します。
返ってきたエラーがnil(エラーなし)か否か判断し、エラー処理を記述します。

import (
  "log"
  "os"
)

func main() {
  _, err := os.Open("hoge.txt")
  if err != nil {
    // エラー時の処理
    log.Fatal(err)  // open hoge.txt: no such file or directory
  }
}

エラーメッセージの生成

errorsパッケージのNew関数によるエラーハンドリング

errorsパッケージのerrors.New関数でエラー時のエラーメッセージを生成し、エラー発生時、エラーの内容を通知します。

func New(message string) error
import (
  "errors"
  "fmt"
)

func main() {
  if err := doError(); err != nil {
    fmt.Println("err", err)
  }
  // 以降の処理はしない
}

func doError() error {
  return errors.New("エラー発生!")
}

エラー内容のラップ

ラップとは、あるクラスや関数などが提供するデータを含めて、エラー内容を追加し、別の形で提供することです。

errorsパッケージのWrap関数によるエラーハンドリング

errorsパッケージのWrap関数で、ある関数を呼び出した際に取得したエラーに追加情報をラップし、エラーの内容をより分かりやすい別の形で通知します。
ラップすることで、受け取ったエラーだけではわからない内容(「何が」、「どこで」、「どんな処理で」)を付与してエラーメッセージを作成できます。

func Wrap(err error, message string) error
import (
  "github.com/pkg/errors"
  "fmt"
  "os"
)

func main() {
  path := "hoge.txt"
  _, err := os.Open(path)
  if err != nil {
    // エラー時の処理
    fmt.Println(errors.Wrap(err, "failed to open")) // failed to open: open hoge.txt: no such file or directory
  }
  // 以降の処理はしない
}

errorsパッケージのWrapf関数によるエラーハンドリング

Wrap関数と同様に、ある関数を呼び出した際に取得したエラーに追加情報をラップし、エラーの内容をより分かりやすい別の形で通知します。
Wrapf関数の場合、Wrap関数と違い、第2引数で渡した値をフォーマットし出力します。
どういった内容のデータを使用したかをエラー内容に含めることができます。

func Wrapf(err error, format string, args ...interface{}) error
import (
  "github.com/pkg/errors"
  "fmt"
  "os"
)

func main() {
  path := "hoge.txt"
  _, err := os.Open(path)
  if err != nil {
    // エラー時の処理
    fmt.Println(errors.Wrapf(err, "failed to open %q", path)) // failed to open "hoge.txt": open hoge.txt: no such file or directory
  }
  // 以降の処理はしない
}

fmtパッケージのErrorf関数によるエラーハンドリング

fmtパッケージのErrorf関数で、フォーマットを指定したエラーメッセージを生成し、エラー発生時、エラーの内容を通知します。

func Errorf(format string, a ...interface{}) error
import (
  "fmt"
)

func main() {
  if err := doError(); err != nil {
    fmt.Println(err)
  }
  // 以降の処理はしない
}

func doError() error {
  msg := "エラー発生!"
  return fmt.Errorf("err %s", msg)
}

ラップされたエラー内容の抽出

errorsパッケージのUnwrap関数によるエラーの抽出

errorsパッケージのWrap/Wrapf関数でラップされた内容と元のエラーをUnwrap関数で取り出すことができます。

func Unwrap(err error) error
import (
  "github.com/pkg/errors"
  "fmt"
  "os"
)

func main() {
  path := "hoge.txt"
  _, err := os.Open(path)
  if err != nil {
    // エラー時の処理
    wrap := errors.Wrap(err, "failed to open")
    fmt.Println(errors.Unwrap(wrap)) // failed to open: open hoge.txt: no such file or directory
  }
  // 以降の処理はしない
}

errorsパッケージのCause関数によるエラーの抽出

errorsパッケージのWrap/Wrapf関数でラップされたエラーの元のエラーのみをCause関数で取り出すことができます。

func Cause(err error) error
import (
  "github.com/pkg/errors"
  "fmt"
  "os"
)

func main() {
  path := "hoge.txt"
  _, err := os.Open(path)
  if err != nil {
    // エラー時の処理
    wrap := errors.Wrap(err, "failed to open")
    fmt.Println(errors.Cause(wrap)) // open hoge.txt: no such file or directory
  }
  // 以降の処理はしない
}

特定のエラーの探索

errorsパッケージのIs関数によるエラー内容の識別

errorsパッケージのIs関数で指定された第1引数のエラーが第2引数で指定した値に存在するかかどうか判定できます。

func Is(err, target error) bool
import (
  "github.com/pkg/errors"
  "fmt"
  "os"
)

func main() {
  path := "hoge.txt"
  _, err := os.Open(path)
  if err != nil {
    // エラー時の処理
    wrap := errors.Wrap(err, "failed to open")

    // wrapは、errをラップしているので存在する
    fmt.Println(errors.Is(wrap, err)) // true
  }
  // 以降の処理はしない
}

errorsパッケージのAs関数によるエラー内容の抽出

errorsパッケージのAs関数で指定された第1引数のエラーを第2引数で指定したポインタ変数に代入できます。

func As(err error, target interface{}) bool
import (
  "github.com/pkg/errors"
  "fmt"
)

// エラー時に取得するデータの構造体を定義
type ErrorResponse struct {
   status int
   message string
}

// 構造体にerrorインターフェースを実装
func (e ErrorResponse) Error() string {
  return fmt.Sprintf("status: %d, message: %s", e.status, e.message)
}

func main() {
  var errRes ErrorResponse

  err := errRun()

  if errors.As(err, &errRes) {
    fmt.Printf("%s\n", errRes)
  } else {
    fmt.Printf("Unknown error : %s\n", err)
  }
}

// エラーを返す関数
func errRun() error {
  return ErrorResponse{status: 404, message: "Not Fonud"}
}

エラー内容の条件分岐

errorsパッケージのIs関数による分岐

switch文にerrorsパッケージのIs関数を使用することで、エラーの内容に応じて、処理を分岐させることができます。

import (
  "github.com/pkg/errors"
  "fmt"
)

var (
  ErrBadRequest       = errors.New("bad request")
  ErrNoAutentication = errors.New("no authentication")
  ErrAccessDenied    = errors.New("access denied")
  ErrNotFound          = errors.New("not found")
)

func main() {

  err := errRun()

  if err != nil {
    switch {
    case errors.Is(err, ErrBadRequest):
      fmt.Println(err)
    case errors.Is(err, ErrNoAutentication):
      fmt.Println(err)
    case errors.Is(err, ErrAccessDenied):
      fmt.Println(err)
    case errors.Is(err, ErrNotFound):
      fmt.Println(err)
    default:
      fmt.Println(errors.Wrap(err, "other error"))
    }
  }
}

// エラーを返す関数
func errRun() error {
  return ErrNotFound
}

errorsパッケージのCause関数による分岐

switch文にerrorsパッケージのCause関数を使用することで、エラーの内容に応じて、処理を分岐させることができます。

import (
  "github.com/pkg/errors"
  "fmt"
)

var (
  ErrBadRequest       = errors.New("bad request")
  ErrNoAutentication = errors.New("no authentication")
  ErrAccessDenied    = errors.New("access denied")
  ErrNotFound          = errors.New("not found")
)

func main() {

  err := errRun()

  if err != nil {
    switch {
    case errors.Is(err, ErrBadRequest):
      fmt.Println(err)
    case errors.Is(err, ErrNoAutentication):
      fmt.Println(err)
    case errors.Is(err, ErrAccessDenied):
      fmt.Println(err)
    case errors.Is(err, ErrNotFound):
      fmt.Println(err)
    default:
      fmt.Println(errors.Wrap(err, "other error"))
    }
  }
}

// エラーを返す関数
func errRun() error {
  return ErrNotFound
}

まとめ

この記事では、前回に引き続きGo言語の基礎文法(エラー)をまとめました。

少しGo言語をいじってみたいと思ったら、ブラウザ上で実行できる公式チュートリアルでコードを実際に書いてみると雰囲気がつかめるかもしれません。
今後、さらにGo言語のことについて記事にしていきたいと思います。

もし「記事の内容が間違えている!」等ありましたら、Twitterまたはお問い合わせフォームからご連絡いただければと思います。

最後までご覧いただきありがとうございました。

参考サイト

A Tour of Go

Error handling and Go

errors

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

os#Open