Go のエラーとエラーハンドリングと
はじめに
※ 12.16 に投稿していますがシリーズ 2 の 8 日目が空いていたので追加しました。
Go のエラーとエラーハンドリングについては数多と記事が書かれていると思いますが自分なりの言葉でまとめてみたい気持ちになったので書いています。
Go のエラーは愚直にチェックをする必要がありますが、私はそういうところが好きだったりします。
Go のエラー
Go は公式サイト Why does Go not have exceptions? で言及されているように try-catch
のような例外処理がありません。
下記で示すように関数の戻り値として error
が返ってきます。それを都度都度 nil
チェックを実施し処理します。
if err := Func(); err != nil {
// Do something
}
公式ブログ Error handling and Go に記載がある通り、 Go は以下のような interface
で定義されています。
type error interface {
Error() string
}
Go の interface
は定義されているメソッドが構造体で実装されていれば、その interface
を満たしていると判定されます。( = Duck Typing)
つまり、以下のように独自の error
を定義することもできます。
type UniqueError struct {
Msg string
}
func (ue *UniqueError) Error() string {
return ue.Msg
}
Go でエラーを返す
Go でエラーを返すパターンはいくつかあります。
errors.New() を返す
ex)
func Func() {
return errors.New("error")
}
定義
func New(text string) error {
return &errorString{text}
}
type errorString struct {
s string
}
func (e *errorString) Error() string {
return e.s
}
引数で与えた文字列が Error()
メソッドで取得できるエラーが生成できます。
fmt.Errorf() を返す
ex)
func Func() error {
return fmt.Errorf("error")
}
if err := Func(); err != nil {
return fmt.Errorf("failed to Func: %w", err)
}
定義
func Errorf(format string, a ...any) error {
p := newPrinter()
p.wrapErrs = true
p.doPrintf(format, a)
s := string(p.buf)
var err error
switch len(p.wrappedErrs) {
case 0:
err = errors.New(s)
case 1:
w := &wrapError{msg: s}
w.err, _ = a[p.wrappedErrs[0]].(error)
err = w
default:
if p.reordered {
sort.Ints(p.wrappedErrs)
}
var errs []error
for i, argNum := range p.wrappedErrs {
if i > 0 && p.wrappedErrs[i-1] == argNum {
continue
}
if e, ok := a[argNum].(error); ok {
errs = append(errs, e)
}
}
err = &wrapErrors{s, errs}
}
p.free()
return err
}
type wrapError struct {
msg string
err error
}
func (e *wrapError) Error() string {
return e.msg
}
func (e *wrapError) Unwrap() error {
return e.err
}
type wrapErrors struct {
msg string
errs []error
}
func (e *wrapErrors) Error() string {
return e.msg
}
func (e *wrapErrors) Unwrap() []error {
return e.errs
}
fmt.Sprintf()
と同じような形式でエラーを生成できます。
%w
を 引数format
に含め a
として error
を渡すことでエラーを Wrap することができます。
また、 実装を眺めていると error
は複数渡せるとわかります。
(恥ずかしながら実装を眺めて初めて知りました。)
※ Wrap と Unwap については後述します。
そのまま返す
ここでは何もせずそのまま error
を返します。
ログを仕込むなどしてもよいでしょう。
ex)
if err := Func(); err != nil {
slog.Warn(err.Error())
return err
}
独自エラー を返す
独自に定義したエラーを返します。
ex)
if err := Func(); err != nil {
return &UniqueError{Msg: err.Error()}
}
なぜエラーってハンドリングする??
エラーをハンドリングする理由はエラーが発生した場合に応じて処理を実行したいからだと思います。
雑に実装するなら下記のようになるでしょうか。
if err := Func(); err != nil {
panic(err)
}
プログラムが続行できないと判断した場合に panic
を実行しランタイムを停止させます。
公式サイト panic にて言及されていますが、大抵の場合はプログラムを実行し続けるように振る舞うように処理するでしょう。
例えばエラーをハンドリングしたあとの処理は以下のようなものが挙げられるでしょう。
- 特定のエラーを無視したい
- 特定のエラーをHTTPステータスコードに変換したい
このように"特定のエラー"に対して処理を行います。
Go において"特定のエラー"を判定するためには errors.Is()
と errors.As()
を利用する必要があります。
errors.Is(err, target error)
Is
はエラーのインスタンスが同一かどうかをチェックする関数です。
以下のような実装例で活用します。
ex) サーバー起動時のエラーチェック
if err := http.ListenAndServe(":8080", nil); err != nil && errors.Is(err, http.ErrServerClosed) {
panic(err)
}
http.ErrServerClosed
var ErrServerClosed = errors.New("http: Server closed")
簡潔に述べると var
で宣言されたエラーかどうか比較していると言えるでしょう。
errors.As(err, target error)
As
はエラーの型に代入可能かどうかをチェックする関数です。
以下のような実装例で活用します。
ex) PostgreSQL のエラーチェック
func Handle(w http.ResponseWriter, r *http.Request) {
client, _ := sql.Open("postgres", "dsn")
if _, err := client.ExecContext(context.Background(), "SQL"); err != nil {
w.WriteHeader(ToHTTPStatus(err))
return
}
w.WriteHeader(http.StatusOK)
}
func ToHTTPStatus(err error) int {
var target *pq.Error
if errors.As(err, &target) {
switch target.SQLState() {
case "23505":
return http.StatusConflict
// ...
default:
return http.StatusInternalServerError
}
}
return http.StatusInternalServerError
}
pq.Error
type Error struct {
Severity string
Code ErrorCode
Message string
Detail string
Hint string
Position string
InternalPosition string
InternalQuery string
Where string
Schema string
Table string
Column string
DataTypeName string
Constraint string
File string
Line string
Routine string
}
type ErrorCode string
// ...
func (err *Error) Error() string {
return "pq: " + err.Message
}
簡潔に述べると独自定義したエラーかどうか比較していると言えるでしょう。
Is と As の実装
errors.Is()
func Is(err, target error) bool {
if target == nil {
return err == target
}
isComparable := reflectlite.TypeOf(target).Comparable()
for {
if isComparable && err == target {
return true
}
if x, ok := err.(interface{ Is(error) bool }); ok && x.Is(target) {
return true
}
switch x := err.(type) {
case interface{ Unwrap() error }:
err = x.Unwrap()
if err == nil {
return false
}
case interface{ Unwrap() []error }:
for _, err := range x.Unwrap() {
if Is(err, target) {
return true
}
}
return false
default:
return false
}
}
}
errors.As()
func As(err error, target any) bool {
if err == nil {
return false
}
if target == nil {
panic("errors: target cannot be nil")
}
val := reflectlite.ValueOf(target)
typ := val.Type()
if typ.Kind() != reflectlite.Ptr || val.IsNil() {
panic("errors: target must be a non-nil pointer")
}
targetType := typ.Elem()
if targetType.Kind() != reflectlite.Interface && !targetType.Implements(errorType) {
panic("errors: *target must be interface or implement error")
}
for {
if reflectlite.TypeOf(err).AssignableTo(targetType) {
val.Elem().Set(reflectlite.ValueOf(err))
return true
}
if x, ok := err.(interface{ As(any) bool }); ok && x.As(target) {
return true
}
switch x := err.(type) {
case interface{ Unwrap() error }:
err = x.Unwrap()
if err == nil {
return false
}
case interface{ Unwrap() []error }:
for _, err := range x.Unwrap() {
if As(err, target) {
return true
}
}
return false
default:
return false
}
}
}
どちらの実装にも共通しているのが Unwrap()
メソッドをチェック/実行しているということです。
switch x := err.(type) {
case interface{ Unwrap() error }:
err = x.Unwrap()
if err == nil {
return false
}
case interface{ Unwrap() []error }:
for _, err := range x.Unwrap() {
if As(err, target) {
return true
}
}
return false
default:
return false
}
Unwrap() メソッド
error
を Is
や As
で検証するとき Unwrap() が呼び出されるということがわかりました。
つまり、 error
が Wrap されているかどうかが重要となります。
※ Wrap とはエラーを新たに作成する際にオリジナルのエラーを保有している状態( = Unwrap() で返せる状態)と言えるでしょう。
errors.New()
関数はその場で error
を作成しているので Wrap は関係ないです。Unwrap() することを前提としていない関数と言えるでしょう。( = Wrap をしたいのであれば適していない関数です。)
fmt.Errorf()
関数は %w
, error
で渡した error
を内部に保有していており Unwrap() でその error
が返されるように実装されています。
ここで問題になってくるのが独自エラーです。
Go における error
は func Error() string
メソッドが構造体に実装されていればエラーとなります。その独自エラーが errors.New()
と同じような使い方であれば Wrap する必要はありません。
しかし、独自エラーを構築する多くの場合はなにかしらの error
を保有するように定義するかと思います。
そのときは、ぜひ Unwrap() メソッドも忘れずに実装してください。
type UniqueError struct {
Msg string
Err error
}
func (ue *UniqueError) Error() string {
return ue.Msg + ":" + ue.Err.Error()
}
func (ue *UniqueError) Unwrap() error {
return ue.Err
}
もしかしたらエラーハンドリングする側がオリジナルのエラーをチェックしているかもしれません。
エラーを保有していてもUnwrap() が実装されていなければ、それはエラーを握りつぶしているも同然です。
おわりに
自分の言葉で Go のエラーについていろいろと書いてみました。
伝えたかったことは独自エラーを作るなら Unwrap() メソッドも忘れずにということです。
誰かのお役に立てれば幸いです。
間違いなどあればご指摘いただけると幸いです。
Discussion