sqlclosecheck

Table of Contents

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

sqlclosecheck は sql.Rowssql.Stmt が適切に閉じられているかをチェックするツールです。

解決する問題

sql.Rowssql.Stmt を閉じ忘れると、DB コネクションプールが枯渇し、アプリケーション全体がハングします。この問題は負荷テストや本番環境でしか発現しないことが多く、原因の特定が困難です。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Before: rows を閉じ忘れ → コネクションリーク
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)
}
// 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)
}
return users, nil
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// After: defer で rows を閉じる
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)
}
return users, rows.Err()
}

設定

公式ドキュメント

設定オプションはありません。有効化するだけで動作します。

1
2
3
4
5
6
# .golangci.yml
version: "2"

linters:
enable:
- sqlclosecheck

オプション

golangci-lint 経由では設定オプションはありません。

サンプル

検出例

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
// sqlclosecheck が警告するパターン

// 1. sql.Rows を閉じていない
func getOrders(ctx context.Context, db *sql.DB) error {
rows, err := db.QueryContext(ctx, "SELECT * FROM orders") // Rows was not closed
if err != nil {
return err
}
for rows.Next() {
// ...
}
return nil
}

// 2. sql.Stmt を閉じていない
func insertUser(ctx context.Context, db *sql.DB, user User) error {
stmt, err := db.PrepareContext(ctx, "INSERT INTO users (name) VALUES ($1)") // Stmt was not closed
if err != nil {
return err
}
_, err = stmt.ExecContext(ctx, user.Name)
return err
}

// 3. エラーパスで閉じ忘れ
func getUser(ctx context.Context, db *sql.DB, id string) (*User, error) {
rows, err := db.QueryContext(ctx, "SELECT * FROM users WHERE id = $1", id)
if err != nil {
return nil, err
}
if !rows.Next() {
return nil, ErrUserNotFound // rows が閉じられていない
}
var u User
if err := rows.Scan(&u.ID, &u.Name); err != nil {
return nil, err // rows が閉じられていない
}
rows.Close()
return &u, nil
}

修正例

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
42
43
// 1. defer rows.Close()
func getOrders(ctx context.Context, db *sql.DB) error {
rows, err := db.QueryContext(ctx, "SELECT * FROM orders")
if err != nil {
return err
}
defer rows.Close()

for rows.Next() {
// ...
}
return rows.Err()
}

// 2. defer stmt.Close()
func insertUser(ctx context.Context, db *sql.DB, user User) error {
stmt, err := db.PrepareContext(ctx, "INSERT INTO users (name) VALUES ($1)")
if err != nil {
return err
}
defer stmt.Close()

_, err = stmt.ExecContext(ctx, user.Name)
return err
}

// 3. defer で全パスをカバー
func getUser(ctx context.Context, db *sql.DB, id string) (*User, error) {
rows, err := db.QueryContext(ctx, "SELECT * FROM users WHERE id = $1", id)
if err != nil {
return nil, err
}
defer rows.Close()

if !rows.Next() {
return nil, ErrUserNotFound
}
var u User
if err := rows.Scan(&u.ID, &u.Name); err != nil {
return nil, err
}
return &u, rows.Err()
}

注意点

  • sql.Rowssql.Stmt に加え、sqlx.NamedStmtpgx のクエリ結果も検出対象
  • defer rows.Close() をエラーチェックの直後に配置するのが最も安全。すべてのリターンパスで確実に閉じられる
  • rowserrcheck と併用すると、rows.Close()rows.Err() の両方を漏れなくチェックできる
  • コネクションプールの枯渇は本番環境で突然発生することが多いため、CI での自動チェックが特に重要

参考リンク