Go
Go4Day 22

Go言語で「なかった」の返し方

何らかの関数が、その結果として「なかった」ということを返す場合について考えてみます。処理自体は成功したが、その結果「なかった」ということを返す場合です。

いくつかのパターンをあげてみます。

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 を書きましょう!