Goのエラー処理を極める：実践ガイド

[![表紙](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3967563/e8f6848f-b37a-41de-9e87-833a5336de40.png)](https://leapcell.io/?lc_t=q_goerrhandle)

Go のエラー設計は「値」であり、エラー処理とは値を比較して意思決定をすることです。

ビジネスロジックでエラーを無視する必要がある場合のみエラーを無視し、それ以外ではエラーを無視してはいけません。

このような設計は理論的にはプログラマーがすべてのエラーを意識的に処理するようにし、プログラムをより堅牢にします。

この記事では、エラーを適切に処理するための実践についてお話しします。

## TL;DR

- ビジネスロジックでエラーを無視する必要がある場合のみ無視し、それ以外はすべてのエラーを処理する
- errors パッケージを使ってエラーをラップしスタック情報を取得し、より正確にエラー情報を出力する。分散システムでは trace_id で同じリクエストのエラーを紐づける
- エラーは一度だけ処理する（ログ出力やエラーのフォールバック処理も含む）
- エラーの抽象レイヤーを統一し、現在のモジュールより上位の抽象レベルのエラーを投げて混乱を招くのを避ける
- 上位設計によって if err != nil の頻度を減らす

## エラーの正確な出力

エラーログは問題の原因を特定するための重要な手段なので、混乱を招かないログの出力が非常に重要です。err を使ってエラースタックログを取得し、問題の特定に役立てるにはどうすればよいでしょうか。

> よく自問してください：このエラー出力は本当に問題の特定に役立つか？

ログを見ても該当エラーが特定できない場合、それはエラーを出力していないのと同じです。

github.com/pkg/errors はスタックを含むラップ機能を提供してくれます。

```go
func callers() *stack {
 const depth = 32
 var pcs [depth]uintptr
 n := runtime.Callers(3, pcs[:])
 var st stack = pcs[0:n]
 return &st
}

func New(message string) error {
 return &fundamental{
  msg:   message,
  stack: callers(),
 }
}
```

スタックの出力は fundamental が Format インターフェースを実装することで行われます。

その後、fmt.Printf("%+v", err) で対応するスタック情報を出力できます。

```go
func (f *fundamental) Format(s fmt.State, verb rune) {
 switch verb {
 case 'v':
  if s.Flag('+') {
   io.WriteString(s, f.msg)
   f.stack.Format(s, verb)
   return
  }
  fallthrough
 case 's':
  io.WriteString(s, f.msg)
 case 'q':
  fmt.Fprintf(s, "%q", f.msg)
 }
}
```

具体的な使用例を見てみましょう。

```go
func foo() error {
 return errors.New("something went wrong")
}

func bar() error {
 return foo() // エラーにスタック情報を付加
}
```

foo で errors.New を呼び出してエラーを生成し、その後 bar を経由してもう一段階ラップしています。

さらに、テスト用のコードを書いてエラーを出力してみます。

```go
func TestBar(t *testing.T) {
 err := bar()
 fmt.Printf("err: %+v\n", err)
}
```

最終的に出力される内容は foo と bar のスタックが付加されています。

```shell
err: something went wrong
golib/examples/writer_good_code/exception.foo
 E:/project/github/go-lib-new/go-lib/examples/writer_good_code/exception/err.go:8
golib/examples/writer_good_code/exception.bar
 E:/project/github/go-lib-new/go-lib/examples/writer_good_code/exception/err.go:12
...
```

最初の行にエラー文字列が正確に出力され、続く行にエラーが発生した場所のスタックが表示されます。

これらの情報によって、エラー情報と発生箇所を正確に特定できます。

## 分散環境下でのエラー追跡

単一マシンでのエラーは正確に出力できますが、実際のプログラムでは多くの並行処理が存在し、どのエラーが同じリクエストに属するかを区別する必要があります。これには trace_id を使います。

自分の用途や望む形式に応じて trace_id を生成し、コンテキストに設定します。

```go
func CtxWithTraceId(ctx context.Context, traceId string) context.Context {
 ctx = context.WithValue(ctx, TraceIDKey, traceId)
 return ctx
}
```

ログ出力時には CtxTraceID を使って traceId を取得します。

```go
func CtxTraceID(c context.Context) string {
 if gc, ok := c.(*gin.Context); ok {
  // ginリクエストからtrace idを取得...
 }
 
 // go contextから取得
 traceID := c.Value(TraceIDKey)
 if traceID != nil {
  return traceID.(string)
 }
 
 // trace idがなければ生成
 return TraceIDPrefix + xid.New().String()
}
```

ログに traceID を追加することで、traceId でログチェーンを簡単に検索でき、問題解決のためのログ検索が容易になります。

## エラーは一度だけ処理する

エラーは一度だけ処理すべきであり、ログ出力もエラー処理の一部です。

ある場所でログを出力しても問題が解決できない場合、適切な方法はエラーにより多くのコンテキスト情報を追加し、プログラムのどこで問題が発生したかを明確にすることであり、エラーを複数回処理するべきではありません。

私自身がエラーログを使い始めた当初、次のような誤解がありました。業務エラーが返された場合にもすぐにログを出力すべきだと考えていました。例えば：

- ユーザーのアカウントやパスワードのエラー
- ユーザーの SMS 認証コードのエラー

これらはユーザーの入力ミスによるものであり、我々が処理すべきことではありません。

本当に注目すべきなのは、プログラムにバグがあってエラーが発生した場合です。

ビジネスロジック上の正常なエラーは、error レベルのログで出力する必要は実際にはありません。エラーのノイズが多すぎると、本当に調査が必要な問題の発見が妨げられます。

## エラーのフォールバック

ここまでの内容で、実際のプロジェクトにおいてエラーを正確に出力し問題の特定をサポートできるようになりましたが、多くの場合、プログラムが「自動的に」エラーを処理して復旧することを望みます。

最も一般的な例はキャッシュの取得に失敗したとき、データベースにフェイルバックすることです。

```go
func GetUser() (*User, error) {
 user, err := getUserFromCache()
 if err == nil {
  return user, nil
 }

 user, err = getUserFromDB()
 if err != nil {
  return nil, err
 }
 return user, nil
}
```

あるいは、トランザクションに失敗した後、補償メカニズムを発動することもあります。たとえば、注文完了後にユーザーへサイト内メッセージを送りたい場合です。

この場面で即時送信が失敗したとしても、このケースではリアルタイム性の要求がそれほど高くないため、非同期で再送する仕組みを用いることができます。

```go
func CompleteOrder(orderID string) error {
 // 注文完了の他のロジック...
 message := Message{}
 err := sendUserMessage(message)
 if err != nil {
  asyncRetrySendUserMessage(message)
 }
 return nil
}
```

## 意図的にエラーを無視する

もし API がエラーを返さないのであれば、呼び出し側はエラー処理に時間を割く必要がありません。そのため、エラーが発生しても呼び出し側が特別な処理を必要としない場合は、エラーを返す必要はありません。コードは正常に動作しているものとして扱えばよいのです。

これが「Null Object パターン」です。本来ならエラーや nil オブジェクトを返すべきところですが、呼び出し側のエラー処理を省略したい場合は空の構造体を返し、呼び出し側でのエラー処理ロジックを不要にします。

イベントハンドラーを使う場面で、存在しないイベントがある場合、「Null Object パターン」を用いることができます。

たとえば通知システムでは、実際に通知を出したくない場合、空オブジェクトを使うことで通知ロジックでの nil 判定を避けることができます。

```go
// Notifierインターフェースの定義
type Notifier interface {
    Notify(message string)
}

// EmailNotifierの具体的な実装
type EmailNotifier struct{}

func (n *EmailNotifier) Notify(message string) {
    fmt.Printf("Sending email notification: %s\n", message)
}

// NullNotifierの空実装
type NullNotifier struct{}

func (n *NullNotifier) Notify(message string) {
    // 何もしない空実装
}
```

もしメソッドがエラーを返すが、呼び出し側で何も処理したくない場合、最も良い方法はエラーを「\_」で受け取ることです。こうすることで、後から開発者がエラー処理を忘れたのか、意図的に無視したのかで迷うことがありません。

```go
func f() {
  // ...
  _ = notify()
}

func notify() error {
  // ...
}
```

## カスタムエラーのラップ

透過的なエラーは、エラー処理とエラー値の生成の間の結合度を下げることができますが、その場合、エラーによるロジック処理にはあまり活用できません。

エラーに基づいてロジック処理を行う場合、エラー自体が API の一部になります。

上層に異なるエラー表示を提供する必要がある場合、Go 1.13 以降では、errors.Is によるエラー判定を積極的に使いましょう。

エラー型については errors.As で判定することができますが、その場合、公開する API 側でもエラーの管理に注意が必要です。

では、このエラー処理方式による結合度を下げる手段はないのでしょうか？

エラーの特徴を統一的に抽出し、呼び出し側がエラーを共通のインターフェースに変換して判定することができます。net パッケージのエラーはこの方式で処理されています。

```go
type Error interface {
 error
 Timeout() bool // このエラーはタイムアウトか？
}
```

そして、net.OpError は Timeout メソッドを実装しており、タイムアウトかどうかを判定し、特定のビジネスロジック処理に用いることができます。

## エラーの抽象レイヤー

現在のモジュールよりも抽象レベルが高いエラーを投げることは避けましょう。たとえば dao 層でデータを取得する際、データベースにレコードが見つからなかった場合は RecordNotFound エラーを返すべきです。しかし、上層の変換を省くために dao 層から直接 APIError を投げるのは適切ではありません。

また、下位の抽象エラーは、現在のレイヤーに合わせてラップし直すべきです。上位でラップ処理した場合、下位でエラー内容が変わっても上位には影響が出ません。

たとえば、ユーザーのログインは最初は mysql をストレージとして使っていて、マッチできない場合はレコード未発見のエラーが発生します。

後に redis でユーザーをマッチさせるようになり、マッチできない場合はキャッシュ未ヒットのエラーとなります。この時、上位レイヤーはストレージの違いを意識せず、すべて「ユーザーが存在しない」と統一してエラーを返すべきです。

## err != nil の削減

if err != nil の判定を減らすには、上位設計での工夫が必要です。一部のエラー処理は下層でラップし、上層には露出させないようにできます。

関数内の循環的複雑度を下げることで、if err != nil の重複チェック回数も減らせます。たとえば関数のロジックをラップし、外部に公開する部分では一度だけエラーを処理すればよいようにします。

```go
func CreateUser(user *User) error {
 // バリデーションでは1つのエラーだけ返す。フラットに複数返すことはしない。
 if err := ValidateUser(user); err != nil {
  return err
 }
}
```

error の状態を構造体内に保持し、ロジックでエラーが発生した場合は何もせず、最後にまとめてエラーを返すことで、ビジネスロジック内に if err != nil を多数挿入する必要がなくなります。

データコピー処理を例に、データソースと書き込み先の設定を受け取り、データコピーを行います。

```go
type CopyDataJob struct {
 source      *DataSourceConfig
 destination *DataSourceConfig

 err error
}

func (job *CopyDataJob) newSrc() {
 if job.err != nil {
  return
 }

 if job.source == nil {
  job.err = errors.New("source is nil")
  return
 }

 // データソースのインスタンス化
}

func (job *CopyDataJob) newDst() {
 if job.err != nil {
  return
 }

 if job.destination == nil {
  job.err = errors.New("destination is nil")
  return
 }

 // 書き込み先データソースのインスタンス化
}

func (job *CopyDataJob) copy() {
 if job.err != nil {
  return
 }

 // データのコピー処理
}

func (job *CopyDataJob) Run() error {
 job.newSrc()
 job.newDst()

 job.copy()

 return job.err
}
```

この方式だと、一度エラーが発生すれば Run 内の後続処理はすべて err をチェックして即リターンされ、最後に job.err でまとめてエラーを外部に返すことができます。

この方法は、主処理の中で if err != nil の記述を減らせますが、実際にはエラー判定を分散させているだけで、本質的な削減にはなりません。そのため、実際の開発でこのテクニックを多用することはあまりありません。

---

### [私たちはLeapcell、Goプロジェクトのホスティングの最適解です。](https://leapcell.io/?lc_t=q_goerrhandle)

[![Leapcell](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3967563/8f849562-b68a-4573-9037-b8d06546d91e.png)](https://leapcell.io/?lc_t=q_goerrhandle)

[Leapcell](https://leapcell.io/?lc_t=q_goerrhandle)は、Webホスティング、非同期タスク、Redis向けの次世代サーバーレスプラットフォームです：

**複数言語サポート**

- Node.js、Python、Go、Rustで開発できます。

**無制限のプロジェクトデプロイ**

- 使用量に応じて料金を支払い、リクエストがなければ料金は発生しません。

**比類のないコスト効率**

- 使用量に応じた支払い、アイドル時間は課金されません。
- 例: $25で6.94Mリクエスト、平均応答時間60ms。

**洗練された開発者体験**

- 直感的なUIで簡単に設定できます。
- 完全自動化されたCI/CDパイプラインとGitOps統合。
- 実行可能なインサイトのためのリアルタイムのメトリクスとログ。

**簡単なスケーラビリティと高パフォーマンス**

- 高い同時実行性を容易に処理するためのオートスケーリング。
- ゼロ運用オーバーヘッド — 構築に集中できます。

[ドキュメントで詳細を確認！](https://docs.leapcell.io/)

[![Try Leapcell](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3967563/3f725be7-1a16-4c4d-97dc-a9311581bc35.png)](https://leapcell.io/?lc_t=q_goerrhandle)

Xでフォローする：[@LeapcellHQ](https://x.com/LeapcellHQ)

---

[ブログでこの記事を読む](https://leapcell.io/blog/go-error-handling-best-practices)