何らかの関数が、その結果として「なかった」ということを返す場合について考えてみます。処理自体は成功したが、その結果「なかった」ということを返す場合です。
いくつかのパターンをあげてみます。
nil
まず始めに return nil, nil
は 危険 です。エラーでない場合は、何らかの non-nil な値(ポインタ)を返すべきです。
Go言語でコードを書く際はエラーを必ず確認し、そして、以下のような例でエラーでなかった場合は、結果が nil でないことを前提としてコードを書きます。このため return nil, nil
だと「ぬるぽ(にるぽ?)」です。
result, err := fooFunction()
if err != nil {
return err
}
// ここに来た場合は result != nil が期待される
result.bar()
よってエラーなしの場合は non-nil を返しましょう。
(プライベートな関数ならありかもしれませんが、パッケージ外部に公開するものは気をつけましょう)
戻り値が多値ではない(つまりエラーの戻りがない)場合も同様で、エラーがないということは失敗しない関数だという期待があるので、nil は返さないようにします。
(戻り値がひとつの場合は nil もありかもしれない)
// エラーを返さない関数だから成功するんだよね!?
result := fooFunction()
// ここに来た場合は result != nil が期待される
result.bar()
では、non-nil にしつつ、どうやって「なかった」を示すのか、というのを続いて見ていきます。
エラー
「なかった」をエラー側で返す。
戻り値でエラーを返している関数の場合は「なかった」をエラー側で返すことができます。
os.Stat ではファイルが存在しない場合はエラーが返ってきます。このとき、ファイルが「なかった」のか、他の何らかのエラーだったのかを区別するために、 os.IsNotExits という関数が用意されています。
info, err := os.Stat("foo.txt")
if err != nil {
if os.IsNotExist(err) {
// ファイルが「なかった」
...
}
// その他のエラー
...
}
...
なお os パッケージには、定数(厳密には変数ですが) ErrNotExist も用意されています。しかし関数を用意しているのは、複数のエラー型(PathError, LinkError, SyscallError)があるためです。
こういった特別な事情がない他のパッケージでは ErrNotExist や ErrNotFound のようなエラー変数がよく使われていると思います。
// ライブラリ側
package foo
// 結果が「なかった」場合
var ErrNotFound = errors.New("not found")
// 使う側
ret, err := foo.Bar()
if err != nil {
if err == foo.ErrNotFound {
// 「なかった」
...
}
// その他のエラー
...
}
...
「なかった」場合と「その他のエラー」を区別したいかどうかは呼び出し元によります。そのため、エラーの一種として返しておいて、区別する手段を用意しておくというのは、扱いを呼び出し元に委ねることができ、合理的でちょうどよい手だと思います。
info, err := os.Stat("foo.txt")
if err != nil {
// エラーを区別する必要がなければ、そのまま扱うだけ
...
}
...
有無をあらわす bool
map ではキーによるアクセスをしたとき、二つ目の引数で有無を取得できます。
// var data map[string]*Foo だとして
foo, ok := data["hoge"]
エラーではなく bool で返したい場合は、この方式も可能です。
ゼロ値や特別な値
結果がポインタではなく値の場合で、ゼロ値や特別な値を返すパターン。
この場合は nil にはならないので「ぬるぽ」は出ません。
例えば strings.Index は文字列のなかに含まれる部分文字列の位置を返す関数です。部分文字列が見つからない場合は -1
が返ります。
ret := strings.Index("chicken", "dmr")
// ret => -1
※ぬるぽは出なくても、戻り値を未検証でこの -1 をスライスで使おうとして panic する可能性はありますが……
string
を返すような、例えば path.Split では空の文字列が使われます。
dir, file := path.Split("myfile.css")
// dir => "", file => "myfile.css"
エラーの場合とは少し異なりますが、定数で「なかった」場合を定義してもよさそうです。
// ライブラリ側
package foo
// 結果が「なかった」場合
const NotFound = -1
...
// 使う側
ret := foo.Bar()
if ret == foo.NotFound {
...
}
...
「なかった」を判定できるオブジェクト
値ではなくポインタで何らかの struct
を結果に返す場合で「なかった」を扱いたい場合は、その struct
に判定用のメソッド(IsEmpty や IsZero など)を用意する手もあります(これはポインタでも値でも、どちらの場合でも使えます)。
type Result struct {
Value int
Foo string
Bar string
}
func (r *Result) IsEmpty() bool {
return r.Value == -1 // 「なかった」場合に -1 が入るとしたら
}
result := fooFunction()
if result.IsEmpty() {
// 結果が「なかった」場合
...
}
// 結果が「あった」場合
...
ただし、このパターンはさほど見かけません。
呼び出し元が IsEmpty で判定しないコードを書いても、処理が継続してよいような場所ではありだと思いますが、呼び出し元が判定をサボった時に panic なり困った自体が起きるようであれば、エラーを使って「チェックを強制」しましょう。
SQLパッケージの例
sql パッケージは、クエリの結果としてハンドラー的なオブジェクトが返ってきて、それを経由して実際の行を取得するようになっています。
sql.Query では sql.Rows が返ってきます。これ自体は、SQLの結果がゼロ行であっても non-nil です。結果が「なかった」場合は sql.Rows.Next が false を返すので、処理のループに入りません。
rows, err := db.Query("SELECT name FROM users")
if err != nil {
return nil, err
}
// rows != nil
defer rows.Close()
// 結果がゼロ行の場合は Next が false
for rows.Next() {
var name string
err = rows.Scan(&name)
...
}
...
sql.QueryRow では sql.Row が返ってきます。これも結果がゼロ行でも non-nil です。結果が「なかった」場合は Scan がエラー sql.ErrNoRows を返します。
row := db.QueryRow("SELECT name FROM users WHERE id = ?", id)
var name string
err := row.Scan(&name)
if err != nil {
if err != sql.ErrNoRows {
// 何らかのエラー
return nil
}
// err == sql.ErrNoRows なので、結果が「なかった」
...
}
...
SQLの場合は上記のようにして「なかった」のかエラーなのか、区別できるようになっています。
まとめ
原則として:
- nil, nil はダメ。エラーでなければ何らかの値かオブジェクトを返そう
パターンとして:
- エラーの一種で「なかった」を返す
- ゼロ値や特別な値
- 結果のオブジェクトが「なかった」を表現する
- 結果と取り出すデータが別になっている
いろいろありますね。
2018年も、楽しく Go を書きましょう!