Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

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

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

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

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした