Go言語のエラー処理とパッケージ管理

エラーは例外ではなく値です。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
パッケージ main

import (
    "errors"
    "fmt"
)

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

func (e NotFoundError) Error() 文字列 {
    return fmt.Sprintf("ユーザー %d not found", e.ID)
}

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

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

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

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

type User struct {
    Name 文字列
    Age  int
}

func main() {
    for _, id := range []int{1, -1, 999} {
        ユーザー, err := findUser(id)
        if err != nil {
            // Determining the Type of Error
            if errors.Is(err, ErrInvalidInput) {
                fmt.Printf("Input エラー (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 エラー: %v\n", err)
            continue
        }
        fmt.Printf("Found: %s (%d)\n", ユーザー.Name, ユーザー.Age)
    }
}

出力:

TEXT
見つかった:Alice (28)
入力エラー(スキップ):findUser: invalid input
ユーザー 999 存在しない

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

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


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

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

GO
type error interface {
    Error() string
}

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

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

GO
パッケージ 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) エラー {
    if age < 0 {
        return fmt.Errorf("invalid age: %d (must be >= 0)", age)
    }
    return nil
}

// Method 3: Wrap the エラー in `fmt.Errorf` (`%w`)
func loadConfig(path 文字列) エラー {
    if path == "" {
        return fmt.Errorf("loadConfig: %w", ErrNotFound)
    }
    return nil
}

// Method 4: Customizing the エラー type
type TimeoutError struct {
    DurationMs int
    Operation  文字列
}

func (e TimeoutError) Error() 文字列 {
    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 クエリ"}
    fmt.Println(err)  // DB クエリ timed out after 5000ms
}
▶ 試してみよう

(2) 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(異なる対象)
    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("The field %s is invalid; value=%v\n", valErr.Field, valErr.Value)
    }

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

出力:

TEXT
フィールド input 無効, 値=
(梱包後)フィールド input 無効

(2) errors.Is 対 errors.As

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

5. パニック/復旧

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

GO
パッケージ main

import "fmt"

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

    // パニックは現在の関数を即座に終了し、スタックの巻き戻しを開始する
    panic("something went terribly wrong")

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

出力:

TEXT
開始
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("The program terminated normally—the panic was recovered.")
}
▶ 試してみよう

出力:

TEXT
10/2 = 5
Error: panic recovered: division by zero (result=0)
プログラムが正常に終了しました——panic に recover だった

(2) 「panic」と「error」のユースケース

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


6. Go Mod パッケージ管理

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

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

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

BASH
# 1. モジュールの初期化
$ go mod init github.com/alice/user-service
go: creating new go.mod: module github.com/alice/user-service

# 2. コード内では import 外部パッケージ
GO
パッケージ main

import (
    "fmt"
    "github.com/google/uuid"  // 外部依存関係
)

func main() {
    id := uuid.New()
    fmt.Printf("Generated UUID: %s\n", id)
}
BASH
# 3. 依存関係を追加し、整理する
$ 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. 生成されたものを確認する go.mod
$ cat go.mod
module github.com/alice/user-service

go 1.22

require github.com/google/uuid v1.6.0

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

GO
// math/calculator.go
package math

// Capitalized = 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 Fields
    Name string
    // Private fields (cannot be accessed directly from outside the package)
    precision int
}
GO
パッケージ main

import "yourmodule/math"

func main() {
    math.Add(1, 2)      // ✅ 公開
    math.Version        // ✅ 公開変数

    // math.helper(5)   // ❌ Private 関数; compilation エラー
    // math.internalVersion  // ❌ プライベート変数

    c := math.Calculator{Name: "basic"}  // ✅ 公開構造体
    // c.precision = 2  // ❌ Private フィールド; compilation エラー
}

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

GO
// errors/errors.go
package errors

import "fmt"

// Public Error Types (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 package cannot be directly referenced)
type internalError struct {
    detail string
}

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

// Expose the 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"
)

// ---------- Incorrect Definition ----------

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

=== プログラムが正常に終了しました ===
100%
flowchart TD
    A[関数の戻り値 error] --> B{err == nil?}
    B -->|はい| C[通常通り処理する]
    B -->|いいえ| D[エラーの種類を判定する]
    D --> E[errors.Is / == 哨兵]
    D --> F[errors.As / 型アサーション]
    D --> G[種類 switch]
    E --> H[特定のセンチネルエラーの処理]
    F --> I[構造化されたエラー情報の抽出]
    G --> J[タイプごとの分岐処理]
    H --> K[戻るまたは再試行]
    I --> K
    J --> K
🔥 よくある間違い: 57行目のUnwrap() errorメソッドは、カスタムエラーをエラーチェーンに参加させるための鍵となります。カスタム型にUnwrap() errorメソッドがない場合、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%