Goのデータベースクエリにおける 'sql: no rows in result set' の処理方法

beginner🔷 Go2026-06-07| Go (Golang)、database/sqlパッケージ、PostgreSQL、MySQL、またはSQLite

Error Message

sql: no rows in result set
#go#sql#database#golang

「行が見つからない」という罠Goでマイクロサービスを構築しており、特定のユーザープロファイルを1件取得する必要があるとします。db.QueryRow()を呼び出し、.Scan()を繋げて記述します。ローカル環境ではすべて正常に動作します。しかし、ユーザーが存在しないID(例:9999)をリクエストすると、ログが500 Internal Server Errorで溢れかえります。その原因は、センチネルエラーへの誤解にあります。

ログに表示される具体的なメッセージは以下の通りです:

sql: no rows in result set

Python(Noneが返る場合がある)やNode.js(undefinedが返る)とは異なり、Goでは行が見つからないことを明示的なエラー状態として扱います。これはデータベースドライバーのバグではありません。クエリ結果が空だった場合に何が起こるべきかを、開発者に正確に決定させるための意図的な設計上の選択です。

ロジックが破綻するポイント標準的なエラーハンドリングでは、nil以外のすべてのエラーを壊滅的な事象として扱うため、ここで失敗することがよくあります。多くの開発者は次のようなコードを記述します:

func GetUserEmail(id int) (string, error) {
    var email string
    // 一見すると完璧なクエリに見えます
    err := db.QueryRow("SELECT email FROM users WHERE id = $1", id).Scan(&email)
    
    if err != nil {
        // 間違い:「見つからない」を「データベースのダウン」と同じように扱っている
        return "", err
    }
    
    return email, nil
}

存在しないIDでこの関数を呼び出すと、関数はnilではないエラーを返します。呼び出し側のハンドラーはエラーを見て、データベースがクラッシュしたと判断し、ユーザーに500 Internal Server Errorを返します。実際には、ユーザーが見つからない場合は404 Not Foundレスポンスをトリガーすべきです。

慣用的な修正方法Go 1.13以降、これを処理する標準的な方法は、errors.Is()を使用してsql.ErrNoRowsセンチネルをチェックすることです。これにより、ビジネスロジックとインフラストラクチャの障害を分離できます。

正しいパターン```

import ( "database/sql" "errors" "fmt" )

func GetUserEmail(id int) (string, error) { var email string err := db.QueryRow("SELECT email FROM users WHERE id = $1", id).Scan(&email)

if err != nil {
    if errors.Is(err, sql.ErrNoRows) {
        // レコード欠落と接続クラッシュを区別する
        return "", fmt.Errorf("ユーザー %d は存在しません", id)
    }
    // 実際の接続タイムアウトや構文エラーを処理する
    return "", fmt.Errorf("データベース障害: %w", err)
}

return email, nil

}


## 仕組みの裏側なぜ`QueryRow`はこのように振る舞うのでしょうか?これは便利なラッパーだからです。クエリの実行が完了する前であっても、即座に`*Row`オブジェクトを返します。`Scan()`はデータベースの結果が実際に変数にマッピングされる場所であるため、結果セットが空であったことをパッケージが報告できる唯一の場所でもあるのです。
常にこれら2つの状態を区別するようにしてください:
- **sql.ErrNoRows:** クエリは正常に実行されましたが、結果が0件でした。- **その他のエラー:** 接続タイムアウト、認証失敗、または不正なSQL構文。## 代替案:db.Query の使用センチネルエラーを完全に避けたい場合は、`db.Query()`を使用します。対照的に、`Query`はイテレータを返し、行が見つからない場合でもエラーをスローしません。

rows, err := db.Query("SELECT email FROM users WHERE id = $1", id) if err != nil { return "", err } defer rows.Close()

if rows.Next() { var email string if err := rows.Scan(&email); err != nil { return "", err } return email, nil }

// 行は見つかりませんでしたが、エラーは発生しません return "", nil


`Query`は柔軟ですが、単一アイテムの検索には依然として`QueryRow`が標準的です。簡潔であり、行のライフサイクルを自動的に管理してくれるからです。
## 検証手順- **エッジケースのテスト:** `-1`や`0`のようなIDでユニットテストを実行します。生のSQLエラーではなく、カスタムエラーが返されることを確認してください。- **ログレベルの確認:** `ErrNoRows`は`INFO`として記録し、実際の接続障害は`ERROR`として記録されていることを確認します。- **ステータスコードの検証:** APIを構築している場合、エンドポイントが一般的な`500`エラーではなく`404`ステータスを返すようになったことを確認します。## 学んだ教訓- `sql.ErrNoRows`はセンチネル値であり、システム障害の兆候ではありません。- エラーハンドリングをクリーンで読みやすく保つために、`errors.Is()`を使用しましょう。- ユーザーにより良いフィードバックを提供するために、`Scan()`のエラーは常にピンポイントで適切に処理してください。

Related Error Notes