#17747active
cmd/vet: check for missing Err calls for bufio.Scanner and sql.Rows
ステータス変更: discussions active
要約
AIによる要約であり、誤りを含む場合があります。
概要
bufio.Scannerとsql.Rowsを使用した際に、ループ終了後に必須となるErr()メソッドの呼び出しが欠落しているコードを検出する、新しいgo vetチェッカーの提案です。これらのAPIは、エラー発生時も正常終了時もループが終わる設計であるため、エラーの見落としが非常に多く発生しています。
ステータス変更
discussions → active
2026年1月28日のProposal Review Meetingで、本提案が再びactiveステータスとなりました。2025年12月にAlan Donovanが長期間のholdを解除し、実装が進められています。現在、bufio.Scanner向けのチェッカーが実装され(CL 730480)、大規模なコーパスでの評価が行われている段階です。
技術的背景
現状の問題点
bufio.Scannerとsql.Rowsは、イテレーションパターンを使うAPIで、以下のような共通の設計になっています:
scanner := bufio.NewScanner(file)
for scanner.Scan() {
// scanner.Text()を処理
}
// ここでscanner.Err()のチェックが必要だが、忘れられることが多い
このAPIではScan()/Next()がfalseを返す理由が2つあります:
- データの終端に到達した(正常終了)
- エラーが発生した(異常終了)
しかし、ループを抜けただけではどちらなのか判別できません。そのため、Err()メソッドを呼んでエラーの有無を確認する必要がありますが、この呼び出しが頻繁に忘れられています。
見落とされやすいエラー:
bufio.Scannerの場合: I/Oエラーに加えて、トークンサイズがbufio.MaxScanTokenSize(デフォルト64KB)を超えた際のErrTooLongエラーsql.Rowsの場合: データベースとの通信エラーや、カーソルの取得エラー
特にErrTooLongは、strings.Readerのような「絶対に失敗しない」入力元を使っている場合でも、入力データが長すぎると発生する可能性があるため、すべてのケースでチェックが推奨されます。
提案された解決策
go/analysisフレームワークを使った新しいアナライザー(scannererr)を実装し、以下を検出します:
bufio.ScannerのScan()がループで使用されている- ループ終了後、
scanner.Err()の呼び出しがない - 同様のパターンを
sql.RowsのNext()にも適用(予定)
偽陽性の削減策:
strings.Readerやbytes.Readerのような「I/Oエラーが発生しない」入力元については、診断を抑制(ただしErrTooLongは依然として発生しうる)os/execパッケージを使った子プロセスのパイプ読み込みは、実装上の制約から偽陽性となりやすいことが判明(72%の真陽性率)
これによって何ができるようになるか
このgo vetチェッカーにより、開発者は以下のような潜在的なバグを早期に発見できます:
- ファイル読み込み中のI/Oエラーを見逃すケース
- 大きな入力データによるバッファオーバーフロー(
ErrTooLong)の見逃し - データベースクエリの途中でエラーが発生したが、一部のデータだけ処理して成功したと誤認するケース
コード例
// Before: エラーチェックが不足しているコード(問題あり)
func readLines(filename string) ([]string, error) {
file, err := os.Open(filename)
if err != nil {
return nil, err
}
defer file.Close()
var lines []string
scanner := bufio.NewScanner(file)
for scanner.Scan() {
lines = append(lines, scanner.Text())
}
// scanner.Err()をチェックしていない!
// ファイルI/Oエラーや行が長すぎるエラーを見逃す
return lines, nil
}
// After: 適切なエラーチェックを追加
func readLines(filename string) ([]string, error) {
file, err := os.Open(filename)
if err != nil {
return nil, err
}
defer file.Close()
var lines []string
scanner := bufio.NewScanner(file)
for scanner.Scan() {
lines = append(lines, scanner.Text())
}
// ループ終了後、必ずErr()をチェック
if err := scanner.Err(); err != nil {
return nil, fmt.Errorf("scan error: %w", err)
}
return lines, nil
}
// sql.Rowsの例
// Before: エラーチェックが不足
func getUsers(db *sql.DB) ([]User, error) {
rows, err := db.Query("SELECT id, name FROM users")
if err != nil {
return nil, 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, err
}
users = append(users, u)
}
// rows.Err()をチェックしていない!
return users, nil
}
// After: 適切なエラーチェックを追加
func getUsers(db *sql.DB) ([]User, error) {
rows, err := db.Query("SELECT id, name FROM users")
if err != nil {
return nil, 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, err
}
users = append(users, u)
}
// ループ終了後、必ずErr()をチェック
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("rows error: %w", err)
}
return users, nil
}
議論のハイライト
- 初期の懸念(2016年): Josh Arianは「
strings.Readerのような絶対に失敗しない入力元では、偽陽性となる」と指摘。しかしAustin Clementsが「ErrTooLongはどんな入力元でも発生しうる」と反論し、全ケースでチェックが必要との合意形成 - 真陽性率の調査(2026年1月): Alan Donovanが約22,000モジュールで実験を実施。
strings.Reader/bytes.Readerを除外した場合でも、真陽性率は72%(25サンプル中18件)で、go vetの目標である95%に届かず。偽陽性の大半はos/execの子プロセスパイプ読み込みだったが、これも「ErrTooLongは発生しうる」との見解でグレーゾーン扱いに - 実践的な証拠(2022年): PleasingFungusの報告では、自社コードベースの約10%で
Err()チェックが欠落しており、そのすべてがバグだったと報告。実務での重要性が裏付けられた - 配布方針の議論: Robert Griescmerは「
go testで実行される高精度チェック」と「手動実行のgo vetでの積極的チェック」の2段階導入を提案。Alan Donovanは「goplsには問題なく追加でき、go testには不向き。go vetへの追加が妥当」と応答 - API設計の根本議論: Axel Wagnerは「
Err()パターンではなくClose() errorにすべきでは」と提案したが、Bryan Millsが「Closeは書き込みの失敗を示すのに対し、Errは読み込みの失敗を示す」と指摘し、現行API設計が妥当と結論