rowserrcheck

Table of Contents

  1. 解決する問題
  2. 設定
  3. オプション
  4. サンプル
    1. 検出例
    2. 修正例
  5. 注意点
  6. 参考リンク

rowserrcheck は sql.Rows のイテレーション後に rows.Err() がチェックされているかを検出するツールです。

解決する問題

rows.Next() のループが終了しても、それが全行を正常に読み終わったのか、途中でエラーが発生して中断したのかは rows.Err() を確認しないと分かりません。rows.Err() のチェック漏れは、データの欠損やサイレントなエラーの原因になります。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Before: rows.Err() をチェックしていない
func ListUsers(ctx context.Context, db *sql.DB) ([]User, error) {
rows, err := db.QueryContext(ctx, "SELECT id, name FROM users")
if err != nil {
return nil, fmt.Errorf("query users: %w", err)
}
defer rows.Close()

var users []User
for rows.Next() {
var u User
if err := rows.Scan(&u.ID, &u.Name); err != nil {
return nil, fmt.Errorf("scan user: %w", err)
}
users = append(users, u)
}
// rows.Err() のチェックがない → ネットワークエラー等で中断しても気づかない
return users, nil
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// After: rows.Err() をチェック
func ListUsers(ctx context.Context, db *sql.DB) ([]User, error) {
rows, err := db.QueryContext(ctx, "SELECT id, name FROM users")
if err != nil {
return nil, fmt.Errorf("query users: %w", err)
}
defer rows.Close()

var users []User
for rows.Next() {
var u User
if err := rows.Scan(&u.ID, &u.Name); err != nil {
return nil, fmt.Errorf("scan user: %w", err)
}
users = append(users, u)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("iterate users: %w", err)
}
return users, nil
}

設定

公式ドキュメント

1
2
3
4
5
6
7
8
9
10
11
# .golangci.yml 設定例
version: "2"

linters:
enable:
- rowserrcheck
settings:
rowserrcheck:
packages:
- database/sql
- github.com/jmoiron/sqlx

オプション

オプション デフォルト 説明
packages []string ["database/sql"] チェック対象のパッケージ。サードパーティの DB ライブラリを追加できる

サンプル

検出例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// rowserrcheck が警告するパターン

// 1. rows.Err() を呼んでいない
func getProducts(ctx context.Context, db *sql.DB) ([]Product, error) {
rows, err := db.QueryContext(ctx, "SELECT * FROM products")
if err != nil {
return nil, err
}
defer rows.Close()

var products []Product
for rows.Next() { // rows.Err must be checked
var p Product
if err := rows.Scan(&p.ID, &p.Name, &p.Price); err != nil {
return nil, err
}
products = append(products, p)
}
return products, nil // rows.Err() がない
}

// 2. sqlx を使用している場合も同様
func getOrders(ctx context.Context, db *sqlx.DB) ([]Order, error) {
rows, err := db.QueryxContext(ctx, "SELECT * FROM orders")
if err != nil {
return nil, err
}
defer rows.Close()

var orders []Order
for rows.Next() {
var o Order
if err := rows.StructScan(&o); err != nil {
return nil, err
}
orders = append(orders, o)
}
return orders, nil // rows.Err() がない
}

修正例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
func getProducts(ctx context.Context, db *sql.DB) ([]Product, error) {
rows, err := db.QueryContext(ctx, "SELECT * FROM products")
if err != nil {
return nil, err
}
defer rows.Close()

var products []Product
for rows.Next() {
var p Product
if err := rows.Scan(&p.ID, &p.Name, &p.Price); err != nil {
return nil, err
}
products = append(products, p)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("iterate products: %w", err)
}
return products, nil
}

func getOrders(ctx context.Context, db *sqlx.DB) ([]Order, error) {
rows, err := db.QueryxContext(ctx, "SELECT * FROM orders")
if err != nil {
return nil, err
}
defer rows.Close()

var orders []Order
for rows.Next() {
var o Order
if err := rows.StructScan(&o); err != nil {
return nil, err
}
orders = append(orders, o)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("iterate orders: %w", err)
}
return orders, nil
}

注意点

  • rows.Next() は内部エラーが発生すると false を返してループを終了する。しかしエラーの内容は rows.Err() でしか取得できない
  • packages オプションで github.com/jmoiron/sqlxgithub.com/jackc/pgx などのサードパーティライブラリも対象に追加できる
  • sqlclosecheck と併用するのが推奨。rows.Close()rows.Err() の両方を網羅的にチェックできる
  • rows.Err()rows.Close() の前に呼ぶか、defer rows.Close() を使用して rows.Err() を明示的にチェックするパターンが一般的

参考リンク