Go database/sql チュートリアル 07 - エラーハンドリング

"Handling Errors" from database/sql tutorial

Go database/sqlチュートリアルのエラーハンドリングの日本語意訳になります。


エラーハンドリング

database/sql でのほぼ全ての操作では最後の返り値としてエラーが返却されます。
それらのエラーを必ずチェックし、無視しないでください。

エラー動作が特殊だったり、何かが追加されているケースはいくつかあり、知っておくべきものもあります。

取得行セットの繰り返しによるエラー

以下のようなコードがあるとします。

for rows.Next() {
	// ...
}
if err = rows.Err(); err != nil {
	// ここでエラーを処理する
}

rows.Err() でのエラーは、 rows.Next() ループ処理内の様々な種類のエラーの結果が入る可能性があります。
ループ処理は正常に終わるだけでなく、何らかの理由によって終了してしまうかもしれません。
そのためループが正常に終了したかどうかを必ずチェックする必要があります。
異常終了の場合は自動的に rows.Close() が呼ばれます。 rows.Close() は複数回呼ばれても問題ありません。

取得行セットのクローズによるエラー

前述の通り、ループが途中終了した場合は sql.Rows を必ず明示的にクローズする必要があります。
ループが正常終了するかエラーが発生すると自動でクローズされますが、間違えてこれをする可能性があります。

for rows.Next() {
	// ...
	break; // おっと、 rows はクローズされていません。メモリリークします...
}
// 普通は "if err = rows.Err()" をしますが、ここでは省略
// ここで(再)クローズしても安全です。
if err = rows.Close(); err != nil {
	// もしエラーだった場合には何をすべきでしょうか?
	log.Println(err)
}

rows.Close() によって返却されるエラーは、「全てのデータベース操作でのエラーをチェックすべきである」という一般的なルールの唯一の例外となります。
rows.Close() がエラーを返却する場合、何をすべきなのか分かりません。エラーメッセージをロギングしたり、panicするのが賢明かもしれませんし、もしそうでないならエラーを無視してください。

QueryRow() のエラー

1行だけを取得する次のコードがあるとします。

var name string
err = db.QueryRow("select name from users where id = ?", 1).Scan(&name)
if err != nil {
	log.Fatal(err)
}
fmt.Println(name)

id = 1 のユーザーがいない場合はどうなるでしょうか?
結果には1行も無く、 .Scan() では name へ値が入らないでしょう。
すると何が起こるでしょうか。

Goでは sql.ErrNoRows という特別なエラー定数を定義しています。 これは QueryRow() による結果が空のときに返却されます。大抵の場合、これを特別なケースとして扱う必要があります。
アプリケーションコードでは空の結果がエラーと見なされない場合が多いので、エラーがこの特別な定数であるかどうかチェックしないと、予期しないエラーが発生するでしょう。
(訳注: 一般的に、他のプログラミング言語や外部ライブラリでは、SQLで空の結果となってもエラーや例外が発生しないことが多いが、 .QueryRow() では空の結果を考慮する必要がある)

クエリによるエラーは、 Scan() が呼ばれるまで延期され、呼ばれた際に返却されます。上記のコードは以下のように書くのが良いでしょう。

var name string
err = db.QueryRow("select name from users where id = ?", 1).Scan(&name)
if err != nil {
	if err == sql.ErrNoRows {
		// 1行もありませんが、エラーも発生
	} else {
		log.Fatal(err)
	}
}
fmt.Println(name)

なぜ空の結果セットがエラーとみなされるのか、と思う人もいるかもしれません。
空の結果には何も問題ありません。
QueryRow() メソッドが実際に行を見つけることできたかどうかを呼び出し元が区別できるように、この特別なケースを使う必要がある、というのが理由です。
それがなければ、 Scan() は何もしないので、データベースから値が変数へと取得できなかったことに気づかないかもしれません。

QueryRow() を使用しているときのみこのエラーが発生します。
もしこのエラーが他で発生した場合は何かおかしなことをしているでしょう。

特定のデータベースエラーの判定

以下のようなコードがあるとします。

rows, err := db.Query("SELECT someval FROM sometable")
// err は以下のエラーとなります
// ERROR 1045 (28000): Access denied for user 'foo'@'::1' (using password: NO)
if strings.Contains(err.Error(), "Access denied") {
	// パーミッションエラーを処理します
}

しかしながら、これは最善の方法ではありません。
例えば、サーバーが使用している言語によってエラーメッセージの文字列が異なる可能性があります。
エラー番号を比較して何のエラー内容なのかを特定する方がはるかに優れています。

しかしこの機構は database/sql 自体の一部ではないため、ドライバによって異なります。
このチュートリアルで扱っているMySQLドライバでは、次のようなコードを書くことができます。

if driverErr, ok := err.(*mysql.MySQLError); ok {
	if driverErr.Number == 1045 { // エラー番号を直接扱うことができます
		// パーミッションエラーを処理します
	}
}

MySQLError 型は特定のドライバによって提供されており、 .Number フィールドはドライバによって異なるかもしれません。
しかしこの番号はMySQLのエラーメッセージから得たものであるため、データベース固有のものです。ドライバ固有のものではありません。

このコードはまだ汚いです。
1045というマジックナンバーを直接比較するのはコードの臭い(Code Smell)がします。
いくつかのドライバではエラー識別子のリストを提供しています。
例えば、PostgreSQLの pq ドライバではerror.go内にリストがあります。
MySQLにはVividCortexがメンテしているエラー番号の外部パッケージがあります。
このようなリストを使うと、上記のコードはより良くできます。

if driverErr, ok := err.(*mysql.MySQLError); ok {
	if driverErr.Number == mysqlerr.ER_ACCESS_DENIED_ERROR {
		// パーミッションエラーを処理します
	}
}

コネクションエラーの処理

データベースへの接続が切断や強制終了、エラーが発生した場合はどうなるでしょうか。

その際に、失敗したステートメントをリトライするロジックを実装する必要はありません。
database/sql のコネクションプーリングの一部として、失敗したコネクションの処理が組み込まれています。
クエリや他のステートメントを実行した際、そのコネクションが失敗していた場合、Goは新しいコネクションを再オープン(またはコネクションプール内の他のコネクションを取得)し、最大10回リトライします。

しかし、意図しない結果となる可能性があります。
いくつかのエラーは、他の状態のエラーが発生している際にリトライされるかもしれません。
これはドライバ特有の問題であることもあります。
MySQLドライバでの一例では、不要なステートメント(長時間実行中のクエリ等)をキャンセルするために KILL を使用すると、そのステートメントが最大10回リトライされることになります。


前章: プリペアドステートメントの使用
次章: NULLの扱い方

 
comments powered by Disqus