nil は nil

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.
via The Go Programming Language Specification

であると記されている。

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

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

だから本当は

if err != nil {
    ...
}

なんかも

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

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

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 を図式化してみよう4。 こんな感じ。

via 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 は以下のように図式化できる。

via Go Data Structures: Interfaces

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

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

via 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 (ADDISON-WESLEY PROFESSIONAL COMPUTING SERIES)
Alan A.A. Donovan (著), Brian W. Kernighan (著), 柴田 芳樹 (翻訳)
丸善出版 2016-06-20
単行本(ソフトカバー)
4621300253 (ASIN), 9784621300251 (EAN), 4621300253 (ISBN), 9784621300251 (ISBN)
評価     

著者のひとりは(あの「バイブル」とも呼ばれる)通称 “K&R” の K のほうである。この本は Go 言語の教科書と言ってもいいだろう。

reviewed by Spiegel on 2018-10-20 (powered by PA-APIv5)


  1. もちろんこれは言語仕様上の話で実装上は何らかの値をとる。大昔のC言語なんかでは「#define NULL ((void*)0)」みたいな記述もあったが,さすがにそーゆーのはない(よね?)。 [return]
  2. そういう意味では最初に紹介した記事で “Goのnilは(nil, nil)” という部分は間違いではないだろう。 [return]
  3. 拙文「「null 安全」について」を参照のこと。 [return]
  4. 引用元の記事(“Go Data Structures: Interfaces”)では 1 word = 32 bits のシステムとして解説されているのでご注意を。 [return]