Goエラーハンドリングベストプラクティス:ラッピングからリカバリまでの6つのプロダクションパターン
エラー情報の消失がパニックカスケードと出会う時:Goエラーハンドリングの至難の時
午前2時、決済サービスのエラー率が30%に急上昇。ログには "database error: sql: no rows in result set" が溢れているが、どのエンドポイントで、どのSQLクエリで、どのビジネスシナリオで発生したのか全く分からない。さらに悪いことに、ゴルーチン内でキャッチされなかったパニックがHTTPサービス全体を502エラーに陥らせた。4時間の調査の結果、根本原因が判明:エラーが層ごとに飲み込まれ、コンテキスト情報が完全に失われ、パニックにリカバリがなく、監視アラートが無意味だった。
これは決して稀なケースではない。Goの if err != nil はシンプルだ——関数呼び出しのたびにチェックするだけ——しかし、プロダクションのエラーハンドリングはnilチェックだけでは終わらない。エラーチェーンの保持、型付きエラーの実装、パニックのグレースフルリカバリ、エラーミドルウェアチェーンの構築、オブザーバビリティの統合が必要だ。本記事では6つのプロダクション級エラーハンドリングパターンを解説し、堅牢なGoエラーハンドリングシステムの構築を支援する。
主要な学び
- エラーラッピング:
fmt.Errorf("%w", err)で完全なエラーチェーンを保持し、文字列連結でコンテキストを失わない - カスタムエラー型:センチネルエラー + カスタム構造体でビジネスセマンティクスを持つエラー分類を実現
- errors.Is/As:文字列マッチングに代わり、型安全なエラー検査でネストされたエラーチェーンに対応
- パニックリカバリ:HTTPミドルウェアとゴルーチン入口でパニックをリカバリし、カスケード障害を防止
- エラーミドルウェアチェーン:ログ、メトリクス、トレーシングをエラーハンドリングフローに統合
- プロダクションオブザーバビリティ:OpenTelemetryでエラーを分散トレーシングとアラート体系に統合
目次
- エラーハンドリングアーキテクチャ概要
- パターン1:Error Wrapping with fmt.Errorf and %w verb
- パターン2:Custom Error Types with Sentinel Errors
- パターン3:errors.Is / errors.As for Error Inspection
- パターン4:Panic Recovery Middleware
- パターン5:Error Middleware Chain
- パターン6:Production Error Observability with OpenTelemetry
- 5つのよくある落とし穴と解決策
- 10のよくあるエラートラブルシューティング
- 高度な最適化テクニック
- 比較分析
- おすすめオンラインツール
- まとめ
エラーハンドリングアーキテクチャ概要
┌─────────────────────────────────────────────────────────────┐
│ HTTP Request │
└──────────────────────────┬──────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────┐
│ Middleware Chain │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Recovery │→│ Logging │→│ Metrics │→│ Tracing │ │
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
└──────────────────────────┬───────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────┐
│ Business Layer │
│ ┌─────────────┐ ┌──────────────┐ ┌──────────────────┐ │
│ │ fmt.Errorf │ │ Custom Error │ │ errors.Is/As │ │
│ │ (%w wrap) │ │ (sentinel) │ │ (inspection) │ │
│ └─────────────┘ └──────────────┘ └──────────────────┘ │
└──────────────────────────┬───────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────┐
│ Observability Layer │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────┐ │
│ │ OpenTelemetry│ │ Alerting │ │ Dashboard │ │
│ │ Tracing │ │ (PagerDuty)│ │ (Grafana) │ │
│ └──────────────┘ └──────────────┘ └──────────────────┘ │
└──────────────────────────────────────────────────────────────┘
パターン1:Error Wrapping with fmt.Errorf and %w verb
エラーラッピングはGoエラーハンドリングの基石。核心思想:%w 動詞でエラーをラッピングし、完全なエラーチェーンを保持する。文字列連結で元のエラー情報を失わない。
アンチパターン:文字列連結でコンテキストを消失
func getUser(id int) (*User, error) {
row := db.QueryRow("SELECT * FROM users WHERE id = ?", id)
var u User
if err := row.Scan(&u.ID, &u.Name, &u.Email); err != nil {
return nil, fmt.Errorf("scan user failed: %v", err)
}
return &u, nil
}
%v はエラーを文字列に変換する——元の sql: no rows in result set が検査不可能なプレーンテキストになる。呼び出し側は errors.Is や errors.As でエラータイプを判定できない。
正しいアプローチ:%w でエラーチェーンを保持
package repository
import (
"database/sql"
"fmt"
)
type User struct {
ID int
Name string
Email string
}
func GetUser(id int) (*User, error) {
row := db.QueryRow("SELECT * FROM users WHERE id = ?", id)
var u User
if err := row.Scan(&u.ID, &u.Name, &u.Email); err != nil {
return nil, fmt.Errorf("get user id=%d: %w", id, err)
}
return &u, nil
}
func HandleGetUser(id int) {
user, err := GetUser(id)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
log.Printf("user not found: id=%d", id)
return
}
log.Printf("unexpected error: %v", err)
}
_ = user
}
多層ラッピングのエラーチェーン
package service
import (
"fmt"
"myapp/repository"
)
func ProcessOrder(orderID int) error {
user, err := repository.GetUser(orderID)
if err != nil {
return fmt.Errorf("process order %d: %w", orderID, err)
}
_ = user
return nil
}
func HandleOrder(orderID int) {
if err := ProcessOrder(orderID); err != nil {
var sqlErr *sqlite.Error
if errors.As(err, &sqlErr) {
log.Printf("database error: %v", sqlErr)
}
}
}
重要ルール:
- 常に
%vではなく%wでエラーをラッピングする - ラッピング時にビジネスコンテキストを追加(関数名、パラメータ、操作の説明)
- エラーチェーンは最大3-4層まで。それ以上は抽象化レベルの問題
- 境界層でのみ1回ラッピングし、複数層での重複ラッピングを避ける
パターン2:Custom Error Types with Sentinel Errors
カスタムエラー型はエラーにビジネスセマンティクスを与える。核心思想:センチネルエラーで予測可能なエラー状態を定義し、カスタム構造体で構造化コンテキストを運ぶ。
センチネルエラー:予測可能なエラー定数
package apperr
import "errors"
var (
ErrNotFound = errors.New("resource not found")
ErrUnauthorized = errors.New("unauthorized access")
ErrForbidden = errors.New("forbidden operation")
ErrConflict = errors.New("resource conflict")
ErrRateLimited = errors.New("rate limit exceeded")
ErrValidation = errors.New("validation failed")
ErrInternal = errors.New("internal server error")
)
カスタムエラー型:構造化コンテキストの運搬
package apperr
import (
"fmt"
"time"
)
type DomainError struct {
Code string
Message string
Domain string
Timestamp time.Time
Err error
}
func (e *DomainError) Error() string {
if e.Err != nil {
return fmt.Sprintf("[%s] %s: %s: %v", e.Domain, e.Code, e.Message, e.Err)
}
return fmt.Sprintf("[%s] %s: %s", e.Domain, e.Code, e.Message)
}
func (e *DomainError) Unwrap() error {
return e.Err
}
func NewDomainError(domain, code, message string, err error) *DomainError {
return &DomainError{
Code: code,
Message: message,
Domain: domain,
Timestamp: time.Now(),
Err: err,
}
}
ビジネス層でのカスタムエラー使用
package order
import (
"apperr"
"fmt"
)
type OrderService struct {
repo OrderRepository
}
func (s *OrderService) CreateOrder(req CreateOrderRequest) (*Order, error) {
if err := validateOrder(req); err != nil {
return nil, apperr.NewDomainError(
"order",
"VALIDATION_ERROR",
fmt.Sprintf("invalid order request: user_id=%d", req.UserID),
err,
)
}
existing, err := s.repo.FindByUserAndProduct(req.UserID, req.ProductID)
if err != nil {
return nil, apperr.NewDomainError(
"order",
"DB_ERROR",
"failed to check existing order",
err,
)
}
if existing != nil {
return nil, apperr.NewDomainError(
"order",
"CONFLICT",
fmt.Sprintf("duplicate order: user=%d product=%d", req.UserID, req.ProductID),
apperr.ErrConflict,
)
}
order := &Order{
UserID: req.UserID,
ProductID: req.ProductID,
Quantity: req.Quantity,
}
if err := s.repo.Save(order); err != nil {
return nil, apperr.NewDomainError(
"order",
"DB_ERROR",
"failed to save order",
err,
)
}
return order, nil
}
重要ルール:
- センチネルエラーは
errors.Newで定義し、命名はErrで始める - カスタムエラー型は
Error()とUnwrap()メソッドを実装する必要がある - エラーコードは大文字スネークケース(
VALIDATION_ERROR)で、ログ検索に便利 - 各ビジネスドメインは独自のエラーコード名前空間を定義し、衝突を避ける
パターン3:errors.Is / errors.As for Error Inspection
エラー検査はエラーハンドリングの中核操作。核心思想:errors.Is でエラー値をチェックし、errors.As でエラータイプを抽出する。脆弱な文字列マッチングに取って代わる。
errors.Is:エラーチェーン内の特定エラー値をチェック
package handler
import (
"apperr"
"database/sql"
"errors"
"net/http"
)
func GetUserHandler(w http.ResponseWriter, r *http.Request) {
id := r.URL.Query().Get("id")
user, err := userService.GetUser(id)
if err != nil {
switch {
case errors.Is(err, sql.ErrNoRows):
http.Error(w, "user not found", http.StatusNotFound)
case errors.Is(err, apperr.ErrUnauthorized):
http.Error(w, "unauthorized", http.StatusUnauthorized)
case errors.Is(err, apperr.ErrRateLimited):
http.Error(w, "rate limited", http.StatusTooManyRequests)
default:
http.Error(w, "internal error", http.StatusInternalServerError)
}
return
}
writeJSON(w, http.StatusOK, user)
}
errors.As:エラーチェーンから特定タイプを抽出
package handler
import (
"apperr"
"errors"
"net/http"
)
func CreateOrderHandler(w http.ResponseWriter, r *http.Request) {
var req CreateOrderRequest
if err := decodeJSON(r, &req); err != nil {
http.Error(w, "bad request", http.StatusBadRequest)
return
}
order, err := orderService.CreateOrder(req)
if err != nil {
var domainErr *apperr.DomainError
if errors.As(err, &domainErr) {
switch domainErr.Code {
case "VALIDATION_ERROR":
http.Error(w, domainErr.Message, http.StatusBadRequest)
case "CONFLICT":
http.Error(w, domainErr.Message, http.StatusConflict)
case "DB_ERROR":
log.Printf("database error: %v", err)
http.Error(w, "internal error", http.StatusInternalServerError)
default:
http.Error(w, "internal error", http.StatusInternalServerError)
}
return
}
log.Printf("unhandled error: %v", err)
http.Error(w, "internal error", http.StatusInternalServerError)
return
}
writeJSON(w, http.StatusCreated, order)
}
よくある間違い:文字列マッチングによるエラー検査
// ❌ 誤り:文字列マッチングは脆弱で信頼性が低い
if strings.Contains(err.Error(), "not found") {
// ...
}
// ✅ 正しい:errors.Is でエラー値をチェック
if errors.Is(err, apperr.ErrNotFound) {
// ...
}
// ❌ 誤り:型アサーションはエラーチェーンを走査しない
if de, ok := err.(*apperr.DomainError); ok {
// err がラッピングされている場合、ok=false になる
}
// ✅ 正しい:errors.As でエラーチェーンを走査
var de *apperr.DomainError
if errors.As(err, &de) {
// err が多層にラッピングされていても正しく抽出できる
}
重要ルール:
err.Error()で文字列マッチングを絶対にしないerrors.Isはセンチネルエラーのチェック用、errors.Asはカスタムエラータイプの抽出用errors.Asの第2引数はターゲットタイプへのポインタでなければならない- HTTPハンドラ層でエラーからステータスコードへのマッピングを統一的に行い、ビジネス層はエラーを返すだけ
パターン4:Panic Recovery Middleware
パニックはGoの「核兵器」——滅多に使わないが、防御手段は必須。核心思想:HTTPミドルウェアとゴルーチン入口でパニックをリカバリし、カスケード障害を防止する。
HTTPサーバーのパニックリカバリ
package middleware
import (
"log"
"net/http"
"runtime/debug"
)
func Recovery(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if rec := recover(); rec != nil {
stack := debug.Stack()
log.Printf(
"[PANIC] path=%s method=%s error=%v\n%s",
r.URL.Path, r.Method, rec, stack,
)
http.Error(w, "internal server error", http.StatusInternalServerError)
}
}()
next.ServeHTTP(w, r)
})
}
ゴルーチンのパニックリカバリ
package goroutine
import (
"log"
"runtime/debug"
)
func SafeGo(fn func(), panicHandler func(recover any, stack []byte)) {
go func() {
defer func() {
if rec := recover(); rec != nil {
stack := debug.Stack()
log.Printf("[GOROUTINE PANIC] recovered=%v\n%s", rec, stack)
if panicHandler != nil {
panicHandler(rec, stack)
}
}
}()
fn()
}()
}
エラーチャネル付きゴルーチンリカバリ
package worker
import (
"runtime/debug"
)
type PanicError struct {
Recover any
Stack []byte
}
func (e *PanicError) Error() string {
return "goroutine panic recovered"
}
func SafeGoWithErr(fn func() error, errCh chan<- error) {
go func() {
defer func() {
if rec := recover(); rec != nil {
errCh <- &PanicError{
Recover: rec,
Stack: debug.Stack(),
}
}
}()
if err := fn(); err != nil {
errCh <- err
}
}()
}
func ProcessBatch(items []Item) []error {
var errs []error
errCh := make(chan error, len(items))
for _, item := range items {
SafeGoWithErr(func() error {
return processItem(item)
}, errCh)
}
for i := 0; i < len(items); i++ {
if err := <-errCh; err != nil {
errs = append(errs, err)
}
}
return errs
}
Ginフレームワークのリカバリミドルウェア
package middleware
import (
"log"
"net/http"
"runtime/debug"
"github.com/gin-gonic/gin"
)
func GinRecovery() gin.HandlerFunc {
return func(c *gin.Context) {
defer func() {
if rec := recover(); rec != nil {
stack := debug.Stack()
log.Printf(
"[PANIC] path=%s method=%s client_ip=%s error=%v\n%s",
c.Request.URL.Path,
c.Request.Method,
c.ClientIP(),
rec,
stack,
)
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{
"error": "internal server error",
})
}
}()
c.Next()
}
}
重要ルール:
- HTTPサーバーは最外層ミドルウェアにリカバリを必ず追加する
- すべてのゴルーチン入口にリカバリが必要。さもなければパニックがプロセス全体を終了させる
- リカバリ後は必ず完全なスタックトレースを記録する。さもなければ問題を特定できない
- リカバリ後はリクエストの処理を継続せず、500を返す
- 標準ライブラリのパニック(mapの並行書き込みなど)はリカバリしない
パターン5:Error Middleware Chain
エラーミドルウェアチェーンは横断的関心事をエラーハンドリングに統合する。核心思想:ログ、メトリクス、トレーシングをビジネスコードに散らさず、ミドルウェアチェーンで統一的に処理する。
エラーミドルウェアアーキテクチャ
Request → Recovery → Logging → Metrics → Tracing → Handler → Response
│ │ │ │
▼ ▼ ▼ ▼
log panic log error emit counter span error
完全なエラーミドルウェアチェーン実装
package middleware
import (
"log"
"net/http"
"runtime/debug"
"time"
)
type ResponseRecorder struct {
http.ResponseWriter
StatusCode int
Body []byte
}
func (r *ResponseRecorder) WriteHeader(code int) {
r.StatusCode = code
r.ResponseWriter.WriteHeader(code)
}
func (r *ResponseRecorder) Write(b []byte) (int, error) {
r.Body = append(r.Body, b...)
return r.ResponseWriter.Write(b)
}
func ErrorChain(next http.Handler) http.Handler {
return Recovery(
Logging(
Metrics(
Tracing(next),
),
),
)
}
func Recovery(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if rec := recover(); rec != nil {
log.Printf("[PANIC] path=%s error=%v\n%s", r.URL.Path, rec, debug.Stack())
http.Error(w, "internal server error", http.StatusInternalServerError)
}
}()
next.ServeHTTP(w, r)
})
}
func Logging(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
rec := &ResponseRecorder{ResponseWriter: w, StatusCode: http.StatusOK}
next.ServeHTTP(rec, r)
duration := time.Since(start)
if rec.StatusCode >= 400 {
log.Printf(
"[ERROR] method=%s path=%s status=%d duration=%s",
r.Method, r.URL.Path, rec.StatusCode, duration,
)
} else {
log.Printf(
"[INFO] method=%s path=%s status=%d duration=%s",
r.Method, r.URL.Path, rec.StatusCode, duration,
)
}
})
}
func Metrics(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
rec := &ResponseRecorder{ResponseWriter: w, StatusCode: http.StatusOK}
next.ServeHTTP(rec, r)
statusCode := rec.StatusCode
if statusCode >= 500 {
errorCounter.WithLabelValues(r.URL.Path, "5xx").Inc()
} else if statusCode >= 400 {
errorCounter.WithLabelValues(r.URL.Path, "4xx").Inc()
}
requestDuration.WithLabelValues(r.URL.Path).Observe(float64(statusCode))
})
}
func Tracing(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx, span := tracer.Start(r.Context(), r.URL.Path)
defer span.End()
rec := &ResponseRecorder{ResponseWriter: w, StatusCode: http.StatusOK}
next.ServeHTTP(rec, r.WithContext(ctx))
if rec.StatusCode >= 400 {
span.SetAttributes(attribute.Int("http.status_code", rec.StatusCode))
span.SetStatus(codes.Error, http.StatusText(rec.StatusCode))
}
})
}
関数オプションベースのエラーミドルウェア
package middleware
import (
"log"
"net/http"
)
type ErrorMiddlewareOption struct {
LogErrors bool
EmitMetrics bool
TraceErrors bool
OnPanic func(recover any, stack []byte)
}
func ErrorMiddleware(opts ErrorMiddlewareOption) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if rec := recover(); rec != nil {
stack := debug.Stack()
if opts.OnPanic != nil {
opts.OnPanic(rec, stack)
}
log.Printf("[PANIC] %v\n%s", rec, stack)
http.Error(w, "internal server error", http.StatusInternalServerError)
}
}()
rec := &ResponseRecorder{ResponseWriter: w, StatusCode: http.StatusOK}
next.ServeHTTP(rec, r)
if rec.StatusCode >= 400 {
if opts.LogErrors {
log.Printf("[ERROR] path=%s status=%d", r.URL.Path, rec.StatusCode)
}
if opts.EmitMetrics {
errorCounter.WithLabelValues(r.URL.Path).Inc()
}
if opts.TraceErrors {
span := trace.SpanFromContext(r.Context())
span.SetStatus(codes.Error, http.StatusText(rec.StatusCode))
}
}
})
}
}
重要ルール:
- Recoveryは最外層に配置し、すべてのパニックをキャッチする
- Loggingはリクエストコンテキスト(パス、メソッド、ステータスコード、所要時間)を記録する
- Metricsはパスとステータスコード別にエラー率を分類統計する
- Tracingはエラーステータスをスパンに書き込み、分散トレーシングに活用する
- ミドルウェア順序:Recovery → Logging → Metrics → Tracing → Handler
パターン6:Production Error Observability with OpenTelemetry
プロダクションオブザーバビリティはエラーハンドリングのラストマイル。核心思想:OpenTelemetryでエラーを分散トレーシング、メトリクス、ログの3本柱に統合し、検出から特定までのフルチェーンの可視性を実現する。
OpenTelemetryエラートレーシング統合
package telemetry
import (
"context"
"fmt"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/codes"
"go.opentelemetry.io/otel/trace"
)
func RecordError(ctx context.Context, err error, attrs ...attribute.KeyValue) {
span := trace.SpanFromContext(ctx)
if !span.IsRecording() {
return
}
span.SetStatus(codes.Error, err.Error())
span.SetAttributes(attrs...)
span.RecordError(err, trace.WithAttributes(attrs...))
}
func WrapWithSpan(ctx context.Context, operation string, fn func(ctx context.Context) error) error {
ctx, span := otel.Tracer("app").Start(ctx, operation)
defer span.End()
if err := fn(ctx); err != nil {
RecordError(ctx, err,
attribute.String("operation", operation),
)
return err
}
return nil
}
エラートレーシング付きリポジトリ層
package repository
import (
"context"
"database/sql"
"fmt"
"apperr"
"telemetry"
"go.opentelemetry.io/otel/attribute"
)
type UserRepository struct {
db *sql.DB
}
func (r *UserRepository) GetUser(ctx context.Context, id int) (*User, error) {
ctx, span := otel.Tracer("repository").Start(ctx, "UserRepository.GetUser")
defer span.End()
span.SetAttributes(attribute.Int("user.id", id))
row := r.db.QueryRowContext(ctx, "SELECT id, name, email FROM users WHERE id = ?", id)
var u User
if err := row.Scan(&u.ID, &u.Name, &u.Email); err != nil {
if err == sql.ErrNoRows {
span.SetAttributes(attribute.String("error.type", "not_found"))
return nil, fmt.Errorf("get user id=%d: %w", id, apperr.ErrNotFound)
}
telemetry.RecordError(ctx, err,
attribute.String("db.operation", "SELECT"),
attribute.Int("user.id", id),
)
return nil, fmt.Errorf("get user id=%d: %w", id, err)
}
span.SetAttributes(attribute.String("user.name", u.Name))
return &u, nil
}
エラーメトリクス収集
package metrics
import (
"go.opentelemetry.io/otel/metric"
)
var (
errorCounter metric.Int64Counter
errorLatency metric.Float64Histogram
)
func InitMetrics(meter metric.Meter) {
errorCounter, _ = meter.Int64Counter(
"app.errors.total",
metric.WithDescription("Total number of errors"),
)
errorLatency, _ = meter.Float64Histogram(
"app.errors.latency_seconds",
metric.WithDescription("Error handling latency"),
)
}
func RecordErrorMetric(domain, code string) {
errorCounter.Add(context.Background(), 1,
metric.WithAttributes(
attribute.String("domain", domain),
attribute.String("code", code),
),
)
}
エラーアラートルール
groups:
- name: error_alerts
rules:
- alert: HighErrorRate
expr: |
sum(rate(app_errors_total{code=~"5.."}[5m]))
/
sum(rate(http_requests_total[5m]))
> 0.05
for: 2m
labels:
severity: critical
annotations:
summary: "Error rate exceeds 5%"
description: "5xx error rate is {{ $value | humanizePercentage }}"
- alert: PanicDetected
expr: increase(app_errors_total{code="PANIC"}[1m]) > 0
for: 0m
labels:
severity: critical
annotations:
summary: "Panic recovered in production"
description: "{{ $value }} panic(s) detected in the last minute"
重要ルール:
- すべてのエラーは少なくとも1つのトレーシングスパンに記録する
- エラーメトリクスはドメインとコードで分類し、集計分析に役立てる
- アラートルールは4xx(クライアントエラー)と5xx(サーバーエラー)を区別する
- パニックアラートは即座に発火(
for: 0m)、蓄積期間を設けない - ログ、メトリクス、トレースは同じtrace_idで関連付ける
5つのよくある落とし穴と解決策
落とし穴1:エラーの飲み込み
// ❌ 誤り:戻り値の無視
_, _ = io.Copy(dst, src)
// ✅ 正しい:エラーをチェック
written, err := io.Copy(dst, src)
if err != nil {
return fmt.Errorf("copy data: %w", err)
}
落とし穴2:ラッピング時の元の情報消失
// ❌ 誤り:%v でエラーチェーンが失われる
return fmt.Errorf("query failed: %v", err)
// ✅ 正しい:%w でエラーチェーンを保持
return fmt.Errorf("query failed: %w", err)
落とし穴3:ゴルーチン内のパニック未リカバリ
// ❌ 誤り:ゴルーチンパニックがプロセス全体を終了させる
go func() {
result := doSomething()
ch <- result
}()
// ✅ 正しい:ゴルーチン入口でリカバリ
go func() {
defer func() {
if rec := recover(); rec != nil {
log.Printf("goroutine panic: %v\n%s", rec, debug.Stack())
ch <- nil
}
}()
result := doSomething()
ch <- result
}()
落とし穴4:文字列マッチングによるエラー検査
// ❌ 誤り:文字列マッチングは脆弱
if strings.Contains(err.Error(), "timeout") {
// ...
}
// ✅ 正しい:errors.Is でチェック
if errors.Is(err, context.DeadlineExceeded) {
// ...
}
落とし穴5:同じエラーの重複ラッピング
// ❌ 誤り:多層の重複ラッピングで冗長なエラーメッセージ
func a() error {
return fmt.Errorf("a: %w", err)
}
func b() error {
return fmt.Errorf("b: %w", a()) // "b: a: original error"
}
func c() error {
return fmt.Errorf("c: %w", b()) // "c: b: a: original error"
}
// ✅ 正しい:境界層でのみラッピングし、内部はそのまま渡す
func a() error {
return err // 内部はそのまま渡す
}
func b() error {
return a()
}
func c() error {
return fmt.Errorf("process order: %w", b()) // 境界で1回だけラッピング
}
10のよくあるエラートラブルシューティング
| エラー症状 | 考えられる原因 | 調査方法 | 解決策 |
|---|---|---|---|
sql: no rows in result set |
QueryRowの結果なし | SQL WHERE条件を確認 | errors.Is(err, sql.ErrNoRows) で判定 |
context deadline exceeded |
操作のタイムアウト | contextのタイムアウト設定を確認 | タイムアウトの延長またはクエリパフォーマンスの最適化 |
panic: concurrent map writes |
mapの並行書き込み | ゴルーチン間のmap共有を確認 | sync.Map またはミューテックスを使用 |
panic: send on closed channel |
クローズ済みチャネルへの送信 | チャネルのクローズタイミングを確認 | sync.Onceまたは方向チャネルで制御 |
connection refused |
サービス未起動またはポート誤り | サービスステータスとポートを確認 | サービス起動を確認、ネットワーク設定をチェック |
i/o timeout |
ネットワークタイムアウト | ネットワーク接続性を確認 | タイムアウトの延長、ファイアウォールルールの確認 |
record not found |
データが存在しない | クエリ条件を確認 | 「存在しない」と「クエリ失敗」を区別 |
duplicate key value |
ユニーク制約違反 | 挿入データを確認 | errors.As でPGエラーコードを抽出 |
panic: nil pointer dereference |
nilポインタのデリファレンス | ポインタの初期化を確認 | nilチェックの追加、Recoveryミドルウェアを使用 |
too many open files |
ファイルディスクリプタ枯渇 | コネクションプールとファイル操作を確認 | ulimitの増加、コネクションリークの確認 |
高度な最適化テクニック
テクニック1:エラーグルーピングと集約
package apperr
import "strings"
type ErrorGroup struct {
Errors []error
}
func (g *ErrorGroup) Add(err error) {
if err != nil {
g.Errors = append(g.Errors, err)
}
}
func (g *ErrorGroup) Err() error {
if len(g.Errors) == 0 {
return nil
}
return g
}
func (g *ErrorGroup) Error() string {
msgs := make([]string, len(g.Errors))
for i, err := range g.Errors {
msgs[i] = err.Error()
}
return strings.Join(msgs, "; ")
}
func (g *ErrorGroup) Unwrap() []error {
return g.Errors
}
テクニック2:エラーリトライとバックオフ
package retry
import (
"context"
"fmt"
"math"
"time"
)
type Config struct {
MaxAttempts int
BaseDelay time.Duration
MaxDelay time.Duration
Retryable func(error) bool
}
func Do(ctx context.Context, cfg Config, fn func() error) error {
var lastErr error
for attempt := 0; attempt < cfg.MaxAttempts; attempt++ {
if err := fn(); err != nil {
if !cfg.Retryable(err) {
return fmt.Errorf("non-retryable error: %w", err)
}
lastErr = err
delay := time.Duration(
float64(cfg.BaseDelay) * math.Pow(2, float64(attempt)),
)
if delay > cfg.MaxDelay {
delay = cfg.MaxDelay
}
select {
case <-ctx.Done():
return ctx.Err()
case <-time.After(delay):
continue
}
}
return nil
}
return fmt.Errorf("max attempts (%d) exceeded: %w", cfg.MaxAttempts, lastErr)
}
テクニック3:エラーコードからHTTPステータスへのマッピング
package handler
import (
"apperr"
"errors"
"net/http"
)
var codeToStatus = map[string]int{
"NOT_FOUND": http.StatusNotFound,
"VALIDATION_ERROR": http.StatusBadRequest,
"UNAUTHORIZED": http.StatusUnauthorized,
"FORBIDDEN": http.StatusForbidden,
"CONFLICT": http.StatusConflict,
"RATE_LIMITED": http.StatusTooManyRequests,
"DB_ERROR": http.StatusInternalServerError,
}
func MapErrorToHTTP(err error) (int, string) {
var domainErr *apperr.DomainError
if errors.As(err, &domainErr) {
if status, ok := codeToStatus[domainErr.Code]; ok {
return status, domainErr.Message
}
}
if errors.Is(err, apperr.ErrNotFound) {
return http.StatusNotFound, "resource not found"
}
if errors.Is(err, apperr.ErrUnauthorized) {
return http.StatusUnauthorized, "unauthorized"
}
return http.StatusInternalServerError, "internal server error"
}
テクニック4:構造化エラーログ
package logger
import (
"log/slog"
"apperr"
"errors"
)
func LogError(err error, context ...slog.Attr) {
attrs := []slog.Attr{
slog.String("error.message", err.Error()),
}
var domainErr *apperr.DomainError
if errors.As(err, &domainErr) {
attrs = append(attrs,
slog.String("error.domain", domainErr.Domain),
slog.String("error.code", domainErr.Code),
slog.Time("error.timestamp", domainErr.Timestamp),
)
}
attrs = append(attrs, context...)
slog.LogAttrs(nil, slog.LevelError, "error occurred", attrs...)
}
テクニック5:エラーアサーションヘルパー関数
package apperr
import "errors"
func IsNotFound(err error) bool {
return errors.Is(err, ErrNotFound)
}
func IsConflict(err error) bool {
return errors.Is(err, ErrConflict)
}
func IsRateLimited(err error) bool {
return errors.Is(err, ErrRateLimited)
}
func GetDomainError(err error) (*DomainError, bool) {
var de *DomainError
return de, errors.As(err, &de)
}
func GetCode(err error) string {
var de *DomainError
if errors.As(err, &de) {
return de.Code
}
return "UNKNOWN"
}
比較分析
| 特徴 | fmt.Errorf %w | カスタムエラー型 | errors.Is/As | パニックリカバリ | エラーミドルウェアチェーン | OpenTelemetry |
|---|---|---|---|---|---|---|
| エラーチェーンの保持 | ✅ | ✅ | ✅ | ❌ | ❌ | ✅ |
| ビジネスセマンティクスの付与 | ❌ | ✅ | ❌ | ❌ | ❌ | ✅ |
| 型安全な検査 | ❌ | ✅ | ✅ | ❌ | ❌ | ❌ |
| プロセスクラッシュ防止 | ❌ | ❌ | ❌ | ✅ | ✅ | ❌ |
| 統一的な横断的処理 | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ |
| 分散トレーシング | ❌ | ❌ | ❌ | ❌ | 部分 | ✅ |
| メトリクス収集 | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ |
| 実装複雑度 | 低 | 中 | 低 | 低 | 中 | 高 |
| プロダクション必須度 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
| 適用レイヤー | 全体 | ビジネス | 検査 | ランタイム | HTTP | オブザーバビリティ |
おすすめオンラインツール
- JSONフォーマッター — エラーレスポンスのJSON構造をフォーマットし、APIエラーメッセージを迅速にトラブルシューティング
- ハッシュ計算ツール — リクエスト署名とデータチェックサムを計算し、エラーログのデータ整合性を確保
- cURL→コード変換 — cURLコマンドをGoコードに変換し、HTTPエラーを迅速に再現・デバッグ
まとめ
Goのエラーハンドリングは
if err != nilを追加するだけではない——5つの問いに答える必要がある:エラーはどこから来たか?何のタイプか?どう伝播するか?どうリカバリするか?どう観測するか?fmt.Errorf("%w", err)は「どこから」を、カスタムエラー型は「何のタイプ」を、errors.Is/Asは「どう検査するか」を、リカバリミドルウェアは「どうリカバリするか」を、OpenTelemetryは「どう観測するか」に答える。この6つのパターンをマスターすれば、プロダクション級Goエラーハンドリングの核心的な方法論を身につけたことになる。
参考リンク
- Go Blog: Working with Errors — Go公式エラーハンドリング設計ドキュメント
- Go Blog: Error Handling in Go 1.13 — %w動詞とエラーチェーンの詳細解説
- OpenTelemetry Go SDK — 分散トレーシング統合ガイド
- Effective Go: Errors — Goエラーハンドリングベストプラクティス
- Go 1.24 Release Notes — 最新バージョンのエラーハンドリング改善
ブラウザローカルツールを無料で試す →