こんにちは、イサムです!
本記事では、前回に引き続きGo言語の基本文法(エラー)についてまとめます。
- Go言語の基本文法について知りたい!
- Go言語のエラーハンドリングについて知りたい!
- Go言語の基本文法(エラー)についてのまとめ
エラー
プログラムには、正常系と異常系があります。
正常系とは、プログラムが意図した仕様通りの動作することで、異常系とは、プログラムが意図しない動作をし、実行中のプログラムが中断、終了することです。
この異常系がエラーになります。
エラーの原因は、ソースコードの記述の誤り(バグ)がほとんどです。エラーが発生しない完璧なプログラムを開発するのは、不可能なため、意図しないエラーが発生した時にプログラムが中断、終了しないようにエラーハンドリング(エラー処理)をする必要があります。
そこで、Go言語では、エラーハンドリングするためのerrorsパッケージが標準で備わっているため、これを使用することでプログラムが意図しない動作をしても、プログラムが中断、終了することを回避することができます。
エラーハンドリングで重要なこと
- 必要十分な正しい情報を伝えること
- どの処理の時に、エラーが発生したのか
- エラーの持つ情報でエラーの原因を追える
- デバッグ用などの無駄な情報は残さない(本番環境)
- 受け取り手によって伝え方を変える
- プログラムの開発メンバー向けの内容なのか
- プログラムの保守運用メンバー向けの内容なのか
- プログラムを使用するエンドユーザー向けの内容なのか
エラーハンドリングの基本
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またはお問い合わせフォームからご連絡いただければと思います。
最後までご覧いただきありがとうございました。