Dev Study
← どの言語にもある仕組み — 値・型・数・文字・エラー・メモリ・並行
レッスン 4 / 12·約 8 分·第25期 どの言語にもある仕組み

「無い」をどう表すか — null・nil・Option

リクエストがアプリケーションに届くまで
  1. ブラウザURL入力
  2. DNS名前→IP
  3. ルータ経路選択
  4. ロードバランサ振り分け
  5. Nginx受け口
  6. アプリ処理今ここ
  7. DB保存

同じ1行でも、何が起きるかはその言語が約束した分しか決まっていません。今回は「無い」という約束の違いを、3つの言語で並べて動かします。

まず結論

「値が無い」の表し方は言語ごとに違います。JavaScript は undefined と null という2つの値を持ち、Go は型ごとの nil を持ち、Rust は Option という型で「有るか無いか」を包みます。

型が「無い」を持つと、確かめ忘れを実行前に見つけられます。ただし失敗が消えるわけではなく、取り出す1か所へ移るだけです。Go の nil チェックも、何を nil と数えるかを知らないと素通りします。

? 読む前に予測

Go で、中身が nil の `*MyErr` 型の変数 p を `error` 型の戻り値として返しました。呼び出し側で `find(true) == nil` を表示すると何が出るでしょうか。

当てることが目的ではありません。外した予測ほど記憶に残るので、直感で選んでください。

抽象原理

「無い」を、値で持つか、型で持つか

関数が「見つからなかった」を返す、設定項目が「指定されていない」。どの言語にも「無い」を表す手段がありますが、その約束は大きく3つに分かれます。

1つ目は値として持つやり方です。JavaScript には「無い」を表す値が2つあり、仕様(ECMA-262、2026-09-20 のドラフト)は別々に定義しています。

4.4.13 undefined value — primitive value used when a variable has not been assigned a value

4.4.15 null value — primitive value that represents the intentional absence of any object value

「まだ代入されていない」と「意図して無い」。定義の段階で意味が違います。

2つ目は型ごとに nil を持つやり方で、Go がこれです。ポインタにも map にもインタフェースにも nil がありますが、同じ nil でも、何ができて何ができないかは型ごとに違います。

3つ目は型で包むやり方で、Rust の Option がこれです。Option<i32> は「i32 が有る(Some)か、無い(None)か」を表す別の型で、i32 そのものではありません。中身を使う前に有無を分ける書き方を、型が求めます。

「無い」の置き場所
値として持つJavaScript の undefined と null
型ごとの nilGo のポインタ・map・インタフェース
型で包むRust の Option
確かめ忘れに気づくのは
実行時読んだ値で分岐して初めて分かる
実行時nil の中身は型ごとに違う
コンパイル時ただし unwrap を書けば実行時に止まる
  • ・TypeScript は値の側(JavaScript)に、型の検査を設定で後から載せる。strictNullChecks が off なら null はどの型にも入る
具体実物

JavaScript の2つの「無い」を並べる

Node v24.21.0(aarch64、Docker)で、次の6行を動かしました。

a.js設定ファイル
const o = { a: undefined };
console.log("a" in o, "b" in o, o.a === o.b, null == undefined, null === undefined);
console.log(JSON.stringify({ a: undefined, b: null }));
console.log(typeof null, typeof undefined);
function f(x = 5) { return x } console.log(f(undefined), f(null));
console.log(0 ?? "d", 0 || "d", null ?? "d");
$ node a.jsコマンド出力
true false true true false
{"b":null}
object undefined
5 null
0 d d

1行目の出力が要点です。o.a と o.b は読むとどちらも undefined で === も true ですが、in で聞くと a は有り、b は無い。「undefined が入っている」と「キーが無い」は、読むだけでは区別できません。 null == undefined は true、=== では false です。

出力の4行目は、既定引数が undefined のときだけ効くことを示します。f(null) は null のままです。出力の5行目にある ?? も undefined と null だけを「無い」とみなし、0 はそのまま返します。|| は 0 も空とみなして "d" に置き換えます。「無い」の範囲が演算子ごとに違います。

出力2行目の、JSON にすると undefined のキーが消える件は、プロセスの外へ出るときの話なので第14期で扱います。

✓ ここまでの確認

JavaScript で `function f(x = 5) { return x }` と定義したとき、`f(undefined)` と `f(null)` はそれぞれ何を返しましたか。

抽象原理

nil が型を持つ、Option が型である

Go に戻ります。Go FAQ は Why is my nil error value not equal to nil? という項を立てています。

Under the covers, interfaces are implemented as two elements, a type T and a value V.

An interface value is nil only if the V and T are both unset, (T=nil, V is not set), In particular, a nil interface will always hold a nil type.

error はインタフェースです。nil ポインタを入れると V は空でも T が埋まるので、nil と比べると等しくありません。「nil チェックを書けば安全」は、何が nil と数えられるかを知っていて初めて成り立ちます。

map の nil は別の約束です。仕様は A nil map is equivalent to an empty map except that no elements may be added. と書き、Assigning to an element of a nil map causes a run-time panic. と続けます。読めば空、書けば止まる。

Rust の Option は、この「確かめ忘れ」をコンパイル時に移します。ただし std の文書は unwrap について Panics if the self value equals None. と書きます。Option は失敗を消しません。確かめずに取り出す書き方を、目に見える1語(unwrap)にするだけです。 文書は同じ箇所で、パターンマッチで None を明示的に扱うか unwrap_or などを使うよう勧めています。

具体実物

Go の型つき nil と、Rust の unwrap

Go 1.27.1 で、FAQ と同じ形を動かしました(抜粋)。

main.go(抜粋)設定ファイル
func find(ok bool) error {
var p *MyErr = nil
if ok { return p } // 型つきの nil を error として返す
return nil
}
fmt.Println(find(false) == nil, find(true) == nil)
var m map[string]int
fmt.Println("nil map read:", m["x"], len(m))
m["x"] = 1
$ go run main.goコマンド出力
true false
nil map read: 0 0
true true
recovered: assignment to entry in nil map

find(true) == nil が false です。3行目は、この error から errors.As で *MyErr を取り出すと成功し、取り出した値は nil だったという表示です。「エラーがある」と判定されたのに、中身は nil のポインタです。 最終行は nil map への書き込みで、プログラムが止まります(実演では文言を表示するために受け止めています。止まる仕組みは第9回で扱います)。

rustc 1.98.1(既定の debug ビルド)では、空の Vec の先頭を取り出しました。

$ rustc main.rs -o m && ./mコマンド出力
0
 
thread 'main' (25) panicked at /w/main.rs:5:26:
called `Option::unwrap()` on a `None` value
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

v.first() は Option<&i32> を返します。unwrap_or(0) は無いときの値を自分で決めたので 0 を表示し、unwrap() はそこで止まりました(この環境では exit 101)。止まった場所は main.rs:5:26、つまり unwrap を書いた場所そのものです。失敗は起きていますが、どこで確かめ忘れたかは1か所に特定されています。

✓ ここまでの確認

「Rust で Option を使えば、値が無いせいでプログラムが止まることは無くなる」という主張の扱いとして適切なのはどれですか。

⇄ 2つを見比べる
具体例 A

Go:型つき nil を error で返す

find(true) == nil は false。error インタフェースに *MyErr の型が入ったため。呼び出し側の nil チェックは書かれているのに、成功を失敗と判定する。コンパイラは何も言わない。

go run main.go(1行目)コマンド出力
true false
具体例 B

Rust:None に unwrap を呼ぶ

unwrap_or(0) は 0 を返し、unwrap() は panicked at main.rs:5:26 で止まる。止まる場所は unwrap を書いた行。書かなければコンパイラが有無の分岐を求める。

rustc main.rs -o m && ./m(抜粋)コマンド出力
0
thread 'main' (25) panicked at /w/main.rs:5:26:

どちらもプログラマは「無い場合」を意識して書いています。それでも結果が食い違う理由は、どこにあるでしょうか。

「確かめずに取り出す」を、それぞれのコードのどの文字列から読み取れるかを探してください。

この章のまとめ

  • JavaScript の undefined と null は仕様で定義の違う2つの値。既定引数と ?? は undefined/null だけを、|| は 0 も「無い」とみなす。
  • { a: undefined } の a と、存在しない b は、読むと同じ、in で聞くと違う。
  • Go のインタフェースは型と値の2要素で、両方が空のときだけ nil。nil ポインタを error で返すと != nil になる。
  • Go の nil map は読めば空、書けば止まる。
  • Rust の Option は確かめ忘れをコンパイル時に見つけるが、unwrap は None で止まる。失敗は消えず、書いた1か所に移る。
  • TypeScript の null の検査は strictNullChecks しだい(--strict に含まれる。既定は 5.9.3 で false、6.0 以降で true)。off なら null はどの型にも入る。

自分の現場で確かめる

読んだだけでは身につきません。手元の環境で実際に見てみてください。

理解度チェック

読んだ直後は「分かった」と感じやすいものです。実際に解いて確かめてください。

問題 1 / 3正解: 0

Go で `var m map[string]int` と宣言しただけの m について、仕様と実演に合うものはどれですか。

用語集(4語)
undefined と null(JavaScript)
別々のプリミティブ値。ECMA-262 は undefined を「変数に値が代入されていないときに使われる値」、null を「オブジェクト値が意図して無いことを表す値」と定義する。
nil インタフェース(Go)
インタフェースの値は型 T と値 V の2要素で、両方が空のときだけ nil。nil ポインタを入れると T が埋まり、nil と等しくなくなる。
Option(Rust)
Some(値) か None のどちらかを持つ型。中身を使うには有無の分岐を書くか、unwrap_or などで無いときの値を決める。unwrap は None で止まる(仕組みは第9回で扱う)。
strictNullChecks(TypeScript)
null と undefined の扱いを切り替える設定。off では両者を任意の型に代入でき、on では値を使う前に null・undefined かを確かめる必要がある。