Go: Goのエラー処理とパッケージ管理:`error`インターフェース、`panic`/`recover`、`go

最終更新:2026-08-26

エラーは例外ではなく値である――Goではエラーを通常の戻り値として扱い、この設計により、エラー処理が明示的かつ制御可能で、組み合わせ可能になっている。

Goのエラー処理の考え方とパッケージ管理は、本番環境向けのコードの基盤となるものです。このレッスンでは、Goの2つの、最も過小評価されがちな中核的な機能を習得します。

1. 学習内容



2. マイクロサービスエンジニアの実話

(1) 課題:ネット上のパニックによりサービスがダウンし、アラートグループに500エラーが殺到した

アリスは、マイクロサービスチームのバックエンドエンジニアです。彼女が担当するユーザーサービスで、最近、重大な問題が発生しました:

「先週、ユーザー向けサービスが3回クラッシュしましたが、そのたびにnilポインタの参照が原因でした。Goサービスがパニックを起こすと、プロセス全体がシャットダウンしてしまい、ユーザーは誰もログインできなくなります。プロダクトマネージャーは、あと1回でもクラッシュすればボーナスを減額すると述べていました。」

彼女は故障現場でエラーコードを確認した:

GO
// Bad code: No error handling; it just panics.
func getUserByID(db *sql.DB, id int) *User {
    rows, _ := db.Query("SELECT * FROM users WHERE id = ?", id)
    // If the ID does not exist, rows.Next() returns false.
    // However, directly accessing the value below—the rows.Scan operation on nil—causes a panic.
    var user User
    for rows.Next() {
        rows.Scan(&user.Name, &user.Age)
    }
    return &user
}

3つの問題点:(1) db.Query のエラーが無視されている;(2) 結果の存在確認が行われていない;(3) パニックが発生し、プロセス全体がクラッシュする。

(2) Goの解決策:エラーは値である

GO
// user_service.go
package main

import (
    "errors"
    "fmt"
)

// Custom Error Types
type NotFoundError struct {
    ID int
}

func (e NotFoundError) Error() string {
    return fmt.Sprintf("user %d not found", e.ID)
}

// Error Sentinel
var ErrInvalidInput = errors.New("invalid input")

// Robust Query Function
func findUser(id int) (*User, error) {
    if id <= 0 {
        return nil, fmt.Errorf("findUser: %w", ErrInvalidInput)
    }

    users := map[int]User{
        1: {Name: "Alice", Age: 28},
        2: {Name: "Bob", Age: 32},
    }

    user, ok := users[id]
    if !ok {
        return nil, NotFoundError{ID: id}
    }
    return &user, nil
}

type User struct {
    Name string
    Age  int
}

func main() {
    for _, id := range []int{1, -1, 999} {
        user, err := findUser(id)
        if err != nil {
            // Determining the Type of Error
            if errors.Is(err, ErrInvalidInput) {
                fmt.Printf("Input error (skipped): %v\n", err)
                continue
            }
            var nf NotFoundError
            if errors.As(err, &nf) {
                fmt.Printf("User %d does not exist\n", nf.ID)
                continue
            }
            fmt.Printf("Unknown error: %v\n", err)
            continue
        }
        fmt.Printf("Found: %s (%d)\n", user.Name, user.Age)
    }
}

出力:

TEXT 📖 参照専用
Found: Alice (28)
Input error (skipped): findUser: invalid input
User 999 does not exist

(3) メリット:エラー処理の比較

次元 try-catch の言語 Go のエラー
エラーの種類 例外の制御フロー 通常の戻り値
明示的 暗黙的(catchブロックを見落としやすい) 明示的 if err != nil
パフォーマンス スタック展開のオーバーヘッド 追加のオーバーヘッドなし
組み合わせやすさ 低い(フローが異常な形で中断される) 高い(errを自由に渡せる)
💡 ヒント: Java、C++、Pythonでは例外がスローされるとスタックアンワインディングが発生しますが、Goではerrorは単なるインターフェース値(16バイト)であるため、これを渡す際のオーバーヘッドは事実上ありません。



3. エラーインターフェース

(1) エラーとは何ですか?

GO
type error interface {
    Error() string
}

Error() string メソッドを実装している型はすべてエラーとなります。

▶ サンプル:エラーを生成する4つの方法

GO
package main

import (
    "errors"
    "fmt"
)

// Method 1: errors.New (most commonly used)
var ErrNotFound = errors.New("resource not found")

// Method 2: fmt.Errorf (with formatting)
func validate(age int) error {
    if age < 0 {
        return fmt.Errorf("invalid age: %d (must be >= 0)", age)
    }
    return nil
}

// Method 3: Wrap the error in fmt.Errorf (%w)
func loadConfig(path string) error {
    if path == "" {
        return fmt.Errorf("loadConfig: %w", ErrNotFound)
    }
    return nil
}

// Method 4: Customizing the error type
type TimeoutError struct {
    DurationMs int
    Operation  string
}

func (e TimeoutError) Error() string {
    return fmt.Sprintf("%s timed out after %dms", e.Operation, e.DurationMs)
}

func main() {
    // Method 1
    fmt.Println(ErrNotFound)  // resource not found

    // Method 2
    fmt.Println(validate(-5))  // invalid age: -5 (must be >= 0)

    // Method 3
    fmt.Println(loadConfig(""))  // loadConfig: resource not found

    // Method 4
    err := TimeoutError{DurationMs: 5000, Operation: "DB query"}
    fmt.Println(err)  // DB query timed out after 5000ms
}
▶ 試してみよう

(3) 4つの作成方法の比較

メソッド 関数/構文 目的 エラーの連鎖に対応していますか?
errors.New errors.New("msg") 単純な静的エラー
fmt.Errorf fmt.Errorf("msg %d", n) フォーマットエラー
fmt.Errorf(%w) fmt.Errorf("ctx: %w", err) ラップされたエラー ✅ errors.Is/As
カスタムタイプ struct { ... Error() string } 追加フィールドに関するエラー ✅ カスタム


4. errors.Is / errors.As エラーチェーン

(1) errors.Is: 特定の見張り変数がエラーチェーンに含まれているかどうかを確認します

GO
package main

import (
    "errors"
    "fmt"
)

var ErrDB = errors.New("database error")
var ErrConn = fmt.Errorf("connection failed: %w", ErrDB)

func main() {
    err := fmt.Errorf("query failed: %w", ErrConn)

    // errors.Is searches layer by layer along the %w chain
    fmt.Println(errors.Is(err, ErrDB))    // true
    fmt.Println(errors.Is(err, ErrConn))  // true

    // == Can only match the outermost level
    fmt.Println(err == ErrDB)   // false (different objects)
    fmt.Println(err == ErrConn) // false
}

▶ サンプル:errors.As:チェーンから特定の種類のエラーを抽出する

GO
package main

import (
    "errors"
    "fmt"
)

type ValidationError struct {
    Field string
    Value interface{}
}

func (e ValidationError) Error() string {
    return fmt.Sprintf("validation failed: %s = %v", e.Field, e.Value)
}

func process(input string) error {
    if input == "" {
        return ValidationError{Field: "input", Value: ""}
    }
    return nil
}

func main() {
    err := process("")

    // errors.As: Extracts the ValidationError type from the chain
    var valErr ValidationError
    if errors.As(err, &valErr) {
        fmt.Printf("Field %s is invalid, value=%v\n", valErr.Field, valErr.Value)
    }

    // Also works with wrapping
    wrapped := fmt.Errorf("process failed: %w", err)
    var valErr2 ValidationError
    if errors.As(wrapped, &valErr2) {
        fmt.Printf("(After wrapping) Field %s is invalid\n", valErr2.Field)
    }
}
▶ 試してみよう

出力:

TEXT 📖 参照専用
Field input is invalid, value=
(After wrapping) Field input is invalid

(3) errors.Is 対 errors.As

機能 照合方法 目的
errors.Is(err, target) 等しい (==) 特定のセンチネルエラーが発生したかどうかを確認する
errors.As(err, &target) 型のマッチング エラーチェーンから特定の型のエラーを取得する


5. パニック/復旧

(1) パニック:回復不可能なエラー

GO
package main

import "fmt"

func main() {
    fmt.Println("Start")

    // A panic immediately terminates the current function and begins stack unwinding.
    panic("something went terribly wrong")

    // This line will not be executed
    fmt.Println("End")
}

出力:

TEXT 📖 参照専用
Start
panic: something went terribly wrong

goroutine 1 [running]:
main.main()
        /tmp/main.go:8 +0x...
exit status 2

▶ サンプル:recover(パニック状態から回復する)

GO
package main

import (
    "fmt"
)

// recover is only useful in defer
func safeDivide(a, b int) (result int, err error) {
    defer func() {
        if r := recover(); r != nil {
            err = fmt.Errorf("panic recovered: %v", r)
        }
    }()

    // Intentionally triggering a panic
    if b == 0 {
        panic("division by zero")
    }
    return a / b, nil
}

func main() {
    // Normal call
    if r, err := safeDivide(10, 2); err == nil {
        fmt.Printf("10/2 = %d\n", r)
    }

    // A panic is caught by recover and does not cause a crash
    if r, err := safeDivide(10, 0); err != nil {
        fmt.Printf("Error: %v (result=%d)\n", err, r)
    }

    fmt.Println("Program ended normally—panic was recovered")
}
▶ 試してみよう

出力:

TEXT 📖 参照専用
10/2 = 5
Error: panic recovered: division by zero (result=0)
Program ended normally—panic was recovered

(3) panic と error の使用例

シナリオ error を使用 panic を使用
ユーザー入力の誤り
ファイルが存在しません
ネットワークタイムアウト
ヌルポインタの参照 ❌ (回復不可) ✅ (コードのバグ)
配列のインデックスが範囲外 ❌ (コンパイラによるチェック対象外) ✅ (コードのバグ)
初期化に失敗しました(必須条件)
🔥 よくある間違い: panic + recovertry-catch ブロックのシミュレーションに使用してはいけません。Go の哲学は、「panic は控えめに使い、error をもっと頻繁に使う」というものです。panic は、真の例外的な状況(コードのバグ、初期化の失敗、または回復不可能な状態)にのみ使用すべきです。



6. Go Mod パッケージ管理

(1) Go Modの3つの主要なコマンド

コマンド 機能 一般的な使用例
go mod init <module> モジュールの初期化 新規プロジェクトの開始
go mod tidy 依存関係を整理(不足しているものを追加、不要なものを削除) インポート文を修正した後
go mod add <path>@<ver> 依存関係を追加(Go 1.22以降の新機能) 外部パッケージを追加するには
go get <path>@<ver> 依存関係の追加・更新 従来の方法

▶ サンプル:モジュールの作成と依存関係の追加

BASH
# 1. Initialize module
$ go mod init github.com/alice/user-service
go: creating new go.mod: module github.com/alice/user-service

# 2. Import an external package in the code
GO
package main

import (
    "fmt"
    "github.com/google/uuid"  // external dependency
)

func main() {
    id := uuid.New()
    fmt.Printf("Generated UUID: %s\n", id)
}
BASH
# 3. Add dependencies and organize
$ go mod tidy
go: finding module for package github.com/google/uuid
go: found github.com/google/uuid in github.com/google/uuid v1.6.0

# 4. View the generated go.mod
$ cat go.mod
module github.com/alice/user-service

go 1.22

require github.com/google/uuid v1.6.0

(3) パッケージのエクスポートに関する規則

GO
// math/calculator.go
package math

// Uppercase = Public (accessible to other packages)
func Add(a, b int) int { return a + b }
var Version = "1.0"

// Lowercase first letter = private (visible only within the package)
func helper(x int) int { return x * 2 }
var internalVersion = "0.5"

// Public Structure
type Calculator struct {
    // Public field
    Name string
    // Private field (cannot be accessed directly from outside the package)
    precision int
}
GO
package main

import "yourmodule/math"

func main() {
    math.Add(1, 2)      // ✅ Public
    math.Version        // ✅ Public variable

    // math.helper(5)   // ❌ Private function; compilation error
    // math.internalVersion  // ❌ Private variable

    c := math.Calculator{Name: "basic"}  // ✅ Public struct
    // c.precision = 2  // ❌ Private field; compilation error
}

▶ サンプル:パッケージのエクスポート + エラー型の受け渡し

GO
// apperrors/errors.go
package apperrors

import "fmt"

// Public Error Type (Uppercase)
type BusinessError struct {
    Code    int
    Message string
}

func (e BusinessError) Error() string {
    return fmt.Sprintf("[%d] %s", e.Code, e.Message)
}

// Public Sentinel
var ErrUnauthorized = BusinessError{Code: 401, Message: "unauthorized"}

// Private error (external packages cannot reference directly)
type internalError struct {
    detail string
}

func (e internalError) Error() string {
    return fmt.Sprintf("internal: %s", e.detail)
}

// Public factory function (external packages use internalError indirectly through this function)
func NewInternalError(detail string) error {
    return internalError{detail: detail}
}
▶ 試してみよう

7. 完全な例:堅牢なユーザーサービス

エラー処理、パッケージ管理、およびカスタムエラーを連携させる:

GO
// user_service.go
package main

import (
    "errors"
    "fmt"
)

// ---------- Error Definitions ----------

type NotFoundError struct {
    Resource string
    ID       int
}

func (e NotFoundError) Error() string {
    return fmt.Sprintf("%s with id %d not found", e.Resource, e.ID)
}

type ValidationError struct {
    Field   string
    Message string
}

func (e ValidationError) Error() string {
    return fmt.Sprintf("validation failed: %s - %s", e.Field, e.Message)
}

type DBError struct {
    Operation string
    Err       error
}

func (e DBError) Error() string {
    return fmt.Sprintf("db %s failed: %v", e.Operation, e.Err)
}

func (e DBError) Unwrap() error {
    return e.Err
}

// Sentinel Error
var ErrInternal = errors.New("internal server error")

// ---------- Data Layer (Simulated DB) ----------

type User struct {
    ID   int
    Name string
    Age  int
}

func queryUserFromDB(id int) (*User, error) {
    db := map[int]User{
        1: {ID: 1, Name: "Alice", Age: 28},
        2: {ID: 2, Name: "Bob", Age: 32},
    }
    user, ok := db[id]
    if !ok {
        return nil, NotFoundError{Resource: "user", ID: id}
    }
    return &user, nil
}

// ---------- Service Layer ----------

func GetUser(id int) (*User, error) {
    // panic protection
    defer func() {
        if r := recover(); r != nil {
            fmt.Printf("[PANIC] recovered: %v\n", r)
        }
    }()

    if id <= 0 {
        return nil, ValidationError{
            Field:   "id",
            Message: "must be positive",
        }
    }

    user, err := queryUserFromDB(id)
    if err != nil {
        var nf NotFoundError
        if errors.As(err, &nf) {
            return nil, nf
        }
        return nil, DBError{
            Operation: "queryUserFromDB",
            Err:       err,
        }
    }

    if user.Age < 0 || user.Age > 150 {
        return nil, ValidationError{
            Field:   "age",
            Message: fmt.Sprintf("unexpected age: %d", user.Age),
        }
    }

    return user, nil
}

// ---------- HTTP Layer ----------

func HandleGetUser(id int) {
    user, err := GetUser(id)
    if err != nil {
        var nf NotFoundError
        var ve ValidationError
        var de DBError

        switch {
        case errors.As(err, &nf):
            fmt.Printf("[404] %v\n", err)
        case errors.As(err, &ve):
            fmt.Printf("[400] %v\n", err)
        case errors.As(err, &de):
            fmt.Printf("[500] db error: %v\n", de)
            fmt.Printf("[500] Internal: %+v\n", de.Err)
        default:
            fmt.Printf("[500] %v\n", err)
        }
        return
    }
    fmt.Printf("[200] User: %+v\n", user)
}

func main() {
    // Normal
    HandleGetUser(1)

    // Input error (ValidationError with additional information)
    HandleGetUser(0)

    // User does not exist (custom NotFoundError)
    HandleGetUser(999)

    fmt.Println("\n=== Program Exited Normally ===")
}

期待される出力:

TEXT 📖 参照専用
[200] User: &{ID:1 Name:Alice Age:28}
[400] validation failed: id - must be positive
[404] user with id 999 not found

=== Program Exited Normally ===
100%
flowchart TD
    A[Function returns error] --> B{err == nil?}
    B -->|Yes| C[Normal processing]
    B -->|No| D[Determine error type]
    D --> E[errors.Is / == sentinel]
    D --> F[errors.As / type assertion]
    D --> G[type switch]
    E --> H[Handle specific sentinel error]
    F --> I[Extract structured error info]
    G --> J[Branch by type]
    H --> K[Return or retry]
    I --> K
    J --> K
🔥 よくある間違い: DBError 構造体の Unwrap() error メソッドは、カスタムエラーをエラーチェーンに参加させるための鍵となります。カスタム型に Unwrap() メソッドがない場合、errors.Is および errors.As は最外層のみをチェックすることになります。


❓ よくある質問

Q error はどのような型ですか?
A error は、type error interface { Error() string } という組み込みインターフェースです。Error() string メソッドを実装する型はすべて error であり、これは 16 バイトのインターフェース値です。
Q エラーをカスタマイズするにはどうすればよいですか?
A 構造体を定義し、Error() string メソッドを実装してください。エラーの連鎖(errors.Is/As トラバーサル)をサポートしたい場合は、内部のエラーを返す Unwrap() error メソッドを追加してください。
Q panic の後に recover を使用する必要がありますか?
A 必ずしもそうではありません。recoverdeferブロック内でのみ有効であり、ゴルーチン(go func() { defer recover() })の入り口にのみ配置する必要があります。ビジネスロジック内でrecoverを使用しないでください。それはバグを修正するのではなく、隠蔽してしまうだけです。
Q errors.Iserrors.As の違いは何ですか?
A errors.Is(err, target) は、%w チェーンに沿って、レベルごとに(==)値の比較を行います。errors.As(err, &target)は、チェーンに沿って段階的に型アサーションを行い、targetに値を格納します。簡単に言えば、Isは値をチェックし、Asは型を抽出します。
Q go mod は依存関係をどのように管理しますか?
A 基本的なワークフロー:go mod init で初期化 → コードを記述し import を使用 → go mod tidy で自動的にダウンロード・整理 → go.mod および go.sum を使用してバージョンを固定します。Go 1.22以降では、より直感的なgo mod addコマンドが導入されています。
Q パブリック名とプライベート名のルールは何ですか?
A ルールは1つだけです。大文字で始まる名前はパブリック(エクスポート)となり、小文字で始まる名前はプライベートとなります。これは変数、関数、型、構造体のフィールド、および定数に適用されます。public/privateキーワードは存在しません。
Q fmt.Errorf(%w)fmt.Errorf(%v) の違いは何ですか?
A %w は、errors.Is/As で辿ることができるエラーチェーンを持つエラーを生成します。一方、%v は単に文字列をフォーマットし、元のエラーとは無関係な新しいエラーを生成します。
Q 本番環境のコードでエラーを適切に処理するにはどうすればよいですか?
A (1) エラーチェーンを維持するために fmt.Errorf("context: %w", err) をネストする;(2) 追加フィールドを含むビジネスエラー型を定義する;(3) HTTP ハンドラ層でエラーを一元的に解決し、HTTP ステータスコードに変換する;(4) 完全なチェーンをログに記録する (%+v)。

📖 まとめ


📝 練習問題

  1. 基本問題(難易度 ⭐):除数が 0 の場合は errors.New("division by zero") を返し、それ以外の場合は商を返す関数 Divide を定義しなさい。

  2. 上級問題(難易度 ⭐⭐):JSONファイルから設定を読み込み、それができない場合は環境変数にフォールバックする機能を備えたConfigLoaderを実装してください。要件:各エラーレベルをfmt.Errorf(%w)で囲み、呼び出し元がerrors.Isを使用して、エラーが「ファイルが見つからない」か「JSONの解析エラー」かを判別できるようにしてください。

  3. チャレンジ問題(難易度 ⭐⭐⭐)3層のエラー処理アーキテクチャを構築してください:(1) データ層 RepositoryNotFoundError / DBError を返す; (2) サービス層 Service → データ層のエラーをラップし、ValidationError を追加; (3) HTTP ハンドラ → errors.As を使用してエラーを層ごとに解析し、HTTP ステータスコード (404/400/500) にマッピングする。エラー構造には、ビジネスフィールド(ID/フィールド/操作)を含める必要がある。

Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%