Go言語のエラー処理とパッケージ管理
エラーは例外ではなく値です。Goではエラーを通常の戻り値として扱っており、この設計により、エラー処理が明示的かつ制御可能で、組み合わせ可能になっています。
Goのエラー処理の考え方とパッケージ管理は、本番環境向けのコードの基盤となるものです。このレッスンでは、Goの2つの、最も過小評価されがちな中核的な機能を習得します。
1. 学習内容
エラーインターフェースとその作成方法 4 通りerrors.Is/errors.Asでエラーチェーンを確認する- カスタムエラータイプ
panic/recoverの使用例go mod依存関係管理(init/tidy/add)- パッケージのエクスポート規則(大文字 = 公開、小文字 = 非公開)
- 堅牢なエラー処理を備えたユーザーサービスの構築
2. マイクロサービスエンジニアの実話
(1) 課題:ネット上のパニックによりサービスがダウンし、アラートグループに500エラーが殺到した
アリスは、マイクロサービスチームのバックエンドエンジニアです。彼女が担当するユーザーサービスで、最近、重大な問題が発生しました:
「先週、『ユーザー』サービスが3回クラッシュしましたが、そのたびにnilポインタの参照が原因でした。Goサービスがパニックを起こすたびにプロセス全体がシャットダウンし、ユーザーは誰もログインできなくなってしまいます。プロダクトマネージャーは、あと1回でもクラッシュすればボーナスを減額すると述べていました。」
彼女は故障現場でエラーコードを確認した:
// 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の解決策:エラーは値である
// 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)
}
}
出力:
見つかった:Alice (28)
入力エラー(スキップ):findUser: invalid input
ユーザー 999 存在しない
(3) メリット:エラー処理の比較
| ディメンション | try-catch の記述方法 | Go エラー |
|---|---|---|
| エラーの種類 | 例外の制御フロー | 通常の戻り値 |
| 明示的 | 暗黙的(catchブロックを見落としやすい) | 明示的 if err != nil |
| パフォーマンス | スタック展開のオーバーヘッド | 追加のオーバーヘッドなし |
| 組み合わせやすさ | 低い(フローが異常な形で中断される) | 高い(errを自由に渡せる) |
エラーは単なるインターフェース値(16バイト)であるため、これを渡す際のオーバーヘッドは事実上ありません。
3. エラー インターフェース
(1) エラーとは何ですか?
type error interface {
Error() string
}
Error() string メソッドを実装している型はすべてエラーとなります。
▶ サンプル:エラーを生成する4つの方法
パッケージ 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: 特定の見張り変数がエラーチェーンに含まれているかどうかを確認します
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:チェーンから特定の種類のエラーを抽出する
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)
}
}
出力:
フィールド input 無効, 値=
(梱包後)フィールド input 無効
(2) errors.Is 対 errors.As
| 機能 | 照合方法 | 目的 |
|---|---|---|
errors.Is(err, target) |
等しい (==) | 特定のセンチネルエラーが発生したかどうかを確認する |
errors.As(err, &target) |
型のマッチング | エラーチェーンから特定の型のエラーを取得する |
5. パニック/復旧
(1) パニック:回復不可能なエラー
パッケージ main
import "fmt"
func main() {
fmt.Println("Start")
// パニックは現在の関数を即座に終了し、スタックの巻き戻しを開始する
panic("something went terribly wrong")
// This line will not be executed
fmt.Println("End")
}
出力:
開始
panic: something went terribly wrong
goroutine 1 [running]:
main.main()
/tmp/main.go:8 +0x...
exit status 2
▶ サンプル:recover(パニック状態から立ち直る)
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.")
}
出力:
10/2 = 5
Error: panic recovered: division by zero (result=0)
プログラムが正常に終了しました——panic に recover だった
(2) 「panic」と「error」のユースケース
| シナリオ | error を使用 |
panic を使用 |
|---|---|---|
| ユーザー入力の誤り | ✅ | ❌ |
| ファイルが存在しません | ✅ | ❌ |
| ネットワークタイムアウト | ✅ | ❌ |
| ヌルポインタの参照 | ❌ (回復不可) | ✅ (コードのバグ) |
| 配列のインデックスが範囲外 | ❌ (コンパイラによるチェック対象外) | ✅ (コードのバグ) |
| 初期化に失敗しました(Bing 状態) | ❌ | ✅ |
panic + recover を try-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> |
依存関係の追加・更新 | 従来の方法 |
▶ サンプル:モジュールの作成と依存関係の追加
# 1. モジュールの初期化
$ go mod init github.com/alice/user-service
go: creating new go.mod: module github.com/alice/user-service
# 2. コード内では import 外部パッケージ
パッケージ main
import (
"fmt"
"github.com/google/uuid" // 外部依存関係
)
func main() {
id := uuid.New()
fmt.Printf("Generated UUID: %s\n", id)
}
# 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) パッケージのエクスポートに関する規則
// 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
}
パッケージ 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 エラー
}
▶ サンプル:パッケージのエクスポート + エラー型の受け渡し
// 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. 完全な例:堅牢なユーザーサービス
エラー処理、パッケージ管理、およびカスタムエラーを連携させる:
// 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 ===")
}
期待される出力:
[200] User: &{ID:1 Name:Alice Age:28}
[400] validation failed: id - must be positive
[404] user with id 999 not found
=== プログラムが正常に終了しました ===
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
Unwrap() errorメソッドは、カスタムエラーをエラーチェーンに参加させるための鍵となります。カスタム型にUnwrap() errorメソッドがない場合、errors.Isおよびerrors.Asは最外層のみをチェックすることになります。
❓ よくある質問
error はどのような型ですか?error は、type error interface { Error() string } という組み込みインターフェースです。Error() string メソッドを実装する型はすべて error であり、これは 16 バイトのインターフェース値です。Error() string メソッドを実装してください。エラーの連鎖(errors.Is/As トラバーサル)をサポートしたい場合は、内部のエラーを返す Unwrap() error メソッドを追加してください。panic の後に recover を使用する必要がありますか?recoverはdeferブロック内でのみ有効であり、ゴルーチン(go func() { defer recover() })の入り口にのみ配置する必要があります。ビジネスロジック内でrecoverを使用しないでください。それはバグを修正するのではなく、隠蔽してしまうだけです。errors.Is と errors.As の違いは何ですか?errors.Is(err, target) は、%w チェーンに沿って、レベルごとに(==)値の比較を行います。errors.As(err, &target)は、チェーンに沿って段階的に型アサーションを行い、targetに値を格納します。簡単に言えば、Isは値をチェックし、Asは型を抽出します。go mod は依存関係をどのように管理しますか?go mod init で初期化 → コードを記述し import を使用 → go mod tidy で自動的にダウンロード・整理 → go.mod および go.sum を使用してバージョンを固定します。Go 1.22以降では、より直感的なgo mod addコマンドが導入されています。public/privateキーワードは存在しません。fmt.Errorf(%w) と fmt.Errorf(%v) の違いは何ですか?%w は、errors.Is/As で辿ることができるエラーチェーンを持つエラーを生成します。一方、%v は単に文字列をフォーマットし、元のエラーとは無関係な新しいエラーを生成します。fmt.Errorf("context: %w", err) をネストする;(2) 追加フィールドを含むビジネスエラー型を定義する;(3) HTTP ハンドラ層でエラーを一元的に解決し、HTTP ステータスコードに変換する;(4) 完全なチェーンをログに記録する (%+v)。📖 まとめ
errorは組み込みインターフェースであり、Error() stringを実装する型はすべてerrorとなります。- エラーを生成する4つの方法:
errors.New/fmt.Errorf/%wのラッピング / カスタム型 errors.Isは、対象がエラーチェーンに含まれているかどうかを確認します(値の一致)。errors.Asは、エラーチェーンから指定されたタイプのエラーを(タイプマッチングに基づいて)抽出します。panicは回復不可能なエラーに使用されます。recoverはdeferブロック内でのみ有効ですgo mod init/tidy/add外部依存関係を管理するため- パッケージのエクスポートに関するルールは1つだけです:大文字 = パブリック、小文字 = プライベート
- 生成コード:ネストされたエラー + カスタムエラー型 + 一元的な処理
📝 練習問題
-
基本問題(難易度 ⭐):除数が 0 の場合は
errors.New("division by zero")を返し、それ以外の場合は商を返す関数Divideを定義しなさい。 -
上級問題(難易度 ⭐⭐):JSONファイルから設定を読み込み、それができない場合は環境変数にフォールバックする機能を備えた
ConfigLoaderを実装してください。要件:各エラーレベルをfmt.Errorf(%w)で囲み、呼び出し元がerrors.Isを使用して、エラーが「ファイルが見つからない」か「JSONの解析エラー」かを判別できるようにしてください。 -
課題(難易度:⭐⭐⭐):3層のエラー処理アーキテクチャを構築する:(1) データ層
Repository→NotFoundError/DBErrorを返す; (2) サービス層Service→ データ層のエラーをラップし、ValidationErrorを追加; (3) HTTP ハンドラ →errors.Asを使用してエラーを層ごとに解析し、HTTP ステータスコード(404/400/500)にマッピングする。エラー構造には、ビジネスフィールド(ID/フィールド/操作)を含める必要がある。



