nil は nil

【2020-10-15 追記】 最近 Zenn で以下の記事を書いたので,こちらも併せてどうぞ。

Interface 型の nil 値は「依存の注入(dependency injection)」と絡めて考えると分かりやすいかも知れない。 宣伝でした(笑)

Qiita を覗いてたら

という記事を見かけた。 おそらくは nil の理解のための方便として意図的に書かれているのだろう。 それはそれで悪くないのだが,微妙に危険な香りがするので私なりの解説を記しておく。

nil は nil

たとえば fmt.Printf() 関数などで nil の型と値を取ろうとすると

fmt.Printf("Type: %T, Value: %v", nil, nil)
// Output:
// Type: <nil>, Value: <nil>

などと表示されるので,いかにも nil 型のようなものがあるように見えるが,実際にはこれは「型がない」ことを示している。 同様に値についても,厳密には nil という値ではなく「値がない」ことを示しているのだ。

nil とは何か」をきちんと定義した文章は見かけないが, Go 言語の仕様書には,型 T の変数 x に対して

x is the predeclared identifier nil and T is a pointer, function, slice, map, channel, or interface type.

であると記されている。

Go 言語では,ある型の値が「宣言されていない」状態のことを「ゼロ値(zero value)」と呼んでいる。 たとえば int などの数値型では「ゼロ値」として数値の 0 を, bool では false を,文字列では空文字列をとる。 同じようにポインタ型や interface 型などでは nil を「ゼロ値」としましょう,ということなのである。 このように仕様として定義することで曖昧な状態を排除でき,私達ユーザは安心してその変数を使用することができるわけだ。

したがって nil は状態を表す「識別子」あるいは「表現」に過ぎず1,それ自身は型も値も持たない2。 強いて言うなら(プログラミング言語で最も悪名高いとされる3)「null 参照」の一種だとは言えるだろう。

だから本当は

if err != nil {
    ...
}

なんかも

if !(err is nil) {
    ...
}

みたいな感じに書ければ分かりやすかったのかもしれないが,シンプルを旨とする Go 言語でそんな迂遠な表現がとられるわけもなく,敢えて「nil との同値性(equality)」という表現をとっているわけだ(偏見)。

nil と比較可能な型

nil== または != で比較可能(comparable)な型は以下の通り(ポインタ型を除く)。

  • slice 型
  • map 型
  • 関数型
  • channel 型
  • interface 型

このうち slice, map, および関数の各型は nil との同値性のみ検証できる(nil でないオブジェクト同士は単純比較できない4)。 また interface 型はクセが強い(笑)型なので,後述の通り,取り扱いには若干の注意が必要である。

Interface 型は,型と値への参照を属性に持つ

まずは,以下の簡単なコードを考えてみる。

package main

import (
    "fmt"
    "strconv"
)

type Binary uint64

func (i Binary) Get() uint64 {
    return uint64(i)
}

func (i Binary) String() string {
    return strconv.FormatUint(i.Get(), 2)
}

func main() {
    b := Binary(200)
    fmt.Println(b.String())
    // Output:
    // 11001000
}

変数 b を図式化してみよう5。 こんな感じ。

Go Data Structures: Interfaces

これを覚えておいてね。

ここで main() 関数の中身を以下のように変えてみる。

func main() {
    b := Binary(200)
    s := fmt.Stringer(b)
    fmt.Println(s.String())
}

ちなみに fmt.Stringer 型は以下に定義される interface 型である。

type Stringer interface {
        String() string
}

ゆえに変数 s は以下のように図式化できる。

Go Data Structures: Interfaces

このように interface 型は,型と値への参照を属性に持つオブジェクトとして実装されている。

ただし要素が空の interface{} 型では

Go Data Structures: Interfaces

のように最適化されているらしい。 まぁ,ユーザレベルで両者を区別する必要はないけれど。

interface 型では nil を「ゼロ値」とすると書いたが,そのためには「型と値」の2つの属性が共に nil でなければならない。 値(への参照)だけが nil でも型全体としては nil にならないのである。

これでハマりやすいのがエラーハンドリングである。

エラーハンドリングのハマりどころ

Go 言語の組み込み型である error は以下のように表すことができる。

// The error built-in interface type is the conventional interface for
// representing an error condition, with the nil value representing no error.
type error interface {
    Error() string
}

これを踏まえて,こんなコードを考えてみよう。

package main

import "fmt"

type ErrorObject struct{}

func (m *ErrorObject) Error() string {
    return "I'm error object."
}

func foo() *ErrorObject {
    return nil
}

func bar() error {
    return foo()
}

func main() {
    if err := bar(); err != nil {
        fmt.Printf("%#v is not nil\n", err)
    } else {
        fmt.Printf("%#v is nil\n", err)
    }
}

このコードはコンパイルエラーにならないし完全に動くが,実行結果は

(*main.ErrorObject)(nil) is not nil

となる。 前節まで読んだなら既にお分かりだろうが bar() 関数の返り値の error*ErrorObject という型を持つため nil にならないのである。 したがって err != nil は真(true)となる。

bar() 関数の返り値を正しく評価するには Conversion 構文で型を括りだすか,いっそ foo() 関数を

func foo() error {
    return nil
}

と書き換えるかだろう。 まぁ,後者だよね。 そうすれば実行結果は

<nil> is nil

となる。

ブックマーク

参考図書

photo
プログラミング言語Go
アラン・ドノバン (著), ブライアン・カーニハン (著), 柴田芳樹 (著)
丸善出版 2016-06-20 (Release 2021-07-13)
Kindle版
B099928SJD (ASIN)
評価     

Kindle 版出た! 一部内容が古びてしまったが,この本は Go 言語の教科書と言ってもいいだろう。感想はこちら

reviewed by Spiegel on 2021-05-22 (powered by PA-APIv5)


  1. もちろんこれは言語仕様上の話で実装上は何らかの値をとる。「Go言語における式の評価文脈を理解する」によると本当にゼロで埋めるらしい。 ↩︎

  2. 「型も値も持たない」という意味では最初に紹介した記事の “Goのnilは(nil, nil)” は間違いではなと思う。 ↩︎

  3. 拙文「「null 安全」について」を参照のこと。 ↩︎

  4. slice 型と map 型については reflect.DeepEqual() 関数で nil 以外のオブジェクトと比較可能である。 ↩︎

  5. 引用元の記事(“Go Data Structures: Interfaces”)では 1 word = 32 bits のシステムとして解説されているのでご注意を。 ↩︎