Upgrade to Pro — share decks privately, control downloads, hide ads and more …

nilとは何か 〜interfaceの構造とnil!=nilから理解する〜 / Underst...

Avatar for kuro kuro
February 20, 2026

nilとは何か 〜interfaceの構造とnil!=nilから理解する〜 / Understanding nil in Go Interface Representation and Why nil != nil

Go Conference mini in Sendai 2026で使うスライドです。

Avatar for kuro

kuro

February 20, 2026
Tweet

More Decks by kuro

Other Decks in Programming

Transcript

  1. nilはキーワードではない Goには25個のキーワード がある。if, func, return, go ... nilはキーワードではなく、true, false, iota

    と同じ predeclared identifier(事 前宣言された識別子) キーワードは構文の一部なので変数名には使えない。 一方、predeclared identifierは「最初から宣言されているだけの識別子」な のでシャドウできる。 func := 123 // syntax error nil := 123 // コンパイル通る
  2. nilはデフォルト型を持たない predeclared identifierの中で、デフォルト型を持たない のはnilだけ。 true → bool、iota → int、nil →

    なし var _ = nil // コンパイルエラー : use of untyped nil in variable declaration → nilは文脈から型が推論できないと使えない。 → nil リテラルそのものは型を持たず、これを untyped nil と呼ぶ。 具象型が確定したnil値(例: var p *MyError の p や (*MyError)(nil))を typed nil と呼ぶ。
  3. nilが使える6つの型 nil is a predeclared identifier representing the zero value

    for a pointer, channel, func, interface, map, or slice type. ref: https://pkg.go.dev/builtin#nil これらに共通するのは「内部表現にポインタを含む型」であること。 structやarrayにnilは使えない。 The Go gopher was designed by Renée French.
  4. nilの比較 slice, map, funcはcomparableではない(== が定義されていない)。 ただしnilとの比較だけは仕様で特別に許可されている。 // どの型でも == nil

    は使える var s []int fmt.Println(s == nil) // true // slice同士の比較はできない( nil同士でも) fmt.Println(([]int)(nil) == ([]int)(nil)) // コンパイルエラー : slice can only be compared to nil → comparableではない型でも == nil だけは仕様で例外的に許可されている。
  5. nilが使える型はサイズが違う unsafe.Sizeof で見てみると 型 サイズ( 64bit) 内部構造 pointer 8 bytes

    ポインタ1つ function 8 bytes ポインタ1つ map 8 bytes ポインタ1つ channel 8 bytes ポインタ1つ interface 16 bytes ポインタ 2つ slice 24 bytes ポインタ + len + cap サイズは型が決める。var s []int と宣言した時点で24バイトが確保され、nilはその領域が全部ゼロ の状態。 → nilそれ自体に固有のサイズはなく、代入先の型によってゼロ値のメモリレイアウトが異なる
  6. なぜサイズが違うか 1. pointer / func / map / chan →

    変数の実体はポインタ1つ。nilのときはその8バイトが全部ゼロ。 2. slice → runtime 内部の slice 構造体が3ワード(array pointer, len, cap) 3. interface → 2ワード(型情報 + 値のポインタ) pointer/func /map/chan slice interface ┌────────────┐ ┌────────────┐ ┌────────────┐ │ ptr (8B) │ │ ptr (8B) │ │ type (8B) │ │ = 0 │ │ = 0 │ │ = 0 │ └────────────┘ ├────────────┤ ├────────────┤ nil = 全部0 │ len (8B) │ │ data (8B) │ │ = 0 │ │ = 0 │ ├────────────┤ └────────────┘ │ cap (8B) │ nil = 両方0 │ = 0 │ └────────────┘ nil = 全部0
  7. Russ Cox「Go Data Structures: Interfaces」(2009) 第1部で見たように、interfaceだけが16バイト(2ワード)。 Goのリリース翌月にRuss Coxが書いた解説が、この設計を説明している。 Go's interfaces

    [...] are the most exciting part of Go from a language design point of view. 今でもこの設計は基本的に変わっていない。 ref: https://research.swtch.com/interfaces
  8. interfaceは2ワードのペア interfaceの値は内部的に (型情報, データ) の2つのポインタで構成される。 // runtime/runtime2.go type iface struct

    { tab *itab // 型情報 + メソッドテーブル data unsafe.Pointer // 実体の値へのポインタ } tab が指す itab の中身は以下の通り // internal/abi/iface.go type ITab struct { Inter *InterfaceType // interface自体の型情報 Type *Type // 格納されている具象型の型情報 Hash uint32 // 型スイッチ用のハッシュ Fun [1]uintptr // メソッドテーブル( interfaceのメソッドに対応する具象型の実装への関数ポイン タ) } 空interface(any)は itab の代わりに *_type(型情報のみ)を持つ。→メソッドがないため。
  9. interfaceがnilになる条件 interfaceが == nil になるのは、型情報もデータも両方ゼロ のとき。 == nil ✅ !=

    nil ❌ (T=nil, V=nil) (T=*MyError, V=nil) ┌────────────┐ ┌────────────────┐ │ type: 0 │ ← 型情報なし │ type: *MyError │ ← 型情報あり! ├────────────┤ ├────────────────┤ │ data: 0 │ ← 値なし │ data: 0 │ ← 値はnil └────────────┘ └────────────────┘ nil not nil 型情報が入った時点で、値がnilでもinterface自体はnilではなくなる。
  10. Go公式FAQ An interface value is nil only if the V

    and T are both unset, (T=nil, V is not set). [...] If we store a nil pointer of type *int inside an interface value, the inner type will be *int: (T=*int, V=nil). Such an interface value will therefore be non-nil even when the pointer value V inside is nil. ref: https://go.dev/doc/faq#nil_error → 公式FAQに`Why is my nil error value not equal to nil?`として、載っているというこ と自体が、多くの人がハマる問題だということを示している。
  11. よくあるバグのコード type MyError struct{ msg string } func (e *MyError)

    Error() string { return e.msg } func doSomething() error { var p *MyError = nil if somethingBad() { p = &MyError{"failed"} } return p // ← ここが問題 } func main() { err := doSomething() fmt.Println(err == nil) // false } p はnil(*MyError のゼロ値)だけど、error interfaceに入った時点で (T=*MyError, V=nil) になる。 → 呼び出し側の if err != nil は常にtrueになる
  12. 正しい書き方 func doSomething() error { if somethingBad() { return &MyError{"failed"}

    } return nil // 明示的にnilを返す } To return a proper nil error to the caller, the function must return an explicit nil[...] It's a good idea for functions that return errors always to use the error type in their signature (as we did above) rather than a concrete type such as *MyError, to help guarantee the error is created correctly. ref: https://go.dev/doc/faq#nil_error 戻り値の型が error なら、nilを返すときは明示的に return nil と書く。
  13. nilの何が問題とされてきたか 1. nilポインタをinterfaceに入れると `!= nil` になる → Part 2で見た通り。`error` を返す関数で踏みやすく、バグの元

    になりやすい。。 2. `if err != nil` を毎回書く冗長さ → Goユーザーが最も多く挙げる不満の一つ。Go Developer Surveyでも常に上位。
  14. nilptr / nilinterface(#22729, 2017) Ian Lance Taylorの提案。 nilを6つの種別ごとに分ける。 nilptr, nilinterface,

    nilslice, nilchan, nilfunc, nilmap →例えば、interfaceのnilにはnilinterfaceを使うためコンパイル時に気づく。 →「識別子が6つ増えるのはGoらしくない」という反応が多かった。 ref: https://github.com/golang/go/issues/22729
  15. null(#61489, 2023) Ian Lance Taylorのもう一つの提案。 ポインタ専用の null を導入する案。 if err

    != nil(interface比較)は変えられないから、ポインタ側を変えようという発想。 →`err != nil`を見たとき、「これはnullじゃなくてnilだから、interfaceそのものが空かどう かの判定だ」という語彙レベルの区別。 To be clear, I doubt this proposal will be adopted. But I wanted to write it down as an idea to point to. 本人が「たぶん採用されないかも」と書いている。 ref: https://github.com/golang/go/issues/61489
  16. zero(#61372, 2023) Russ Coxの提案。 Go 1.18でジェネリクスが入って以降、ジェネリック関数でゼロ値を式として1行で書 くには *new(T) が典型(var zero

    T で宣言する方法もある)。 *new(T) is, to put it mildly, an embarrassingly clunky way to write the zero value. zero という汎用ゼロ値識別子を追加して、return zero, err と書けるようにする案。 支持は多かったが(p == zeroと*p == zero両方書けてしまう問題などもあり)結局不採 用。 ref: https://github.com/golang/go/issues/61372
  17. `if err != nil` を減らす試み 「nilの意味論を変える」アプローチのとは少し別の角度で nilに触れる回数そのものを減らす構文変 更も何度か試みられた。 年 提案

    提案者 結果 2018 check / handle Marcel van Lohuizen 複雑すぎると判断 2019 try() 組み込み関数 Robert Griesemer 900超のコメント、断念 2024 ? 演算子 Ian Lance Taylor プロトタイプ実装まで行う も断念 ref: https://go.dev/blog/error-syntax
  18. ? 演算子(#71203, 2025/1) x := strconv.Atoi(a) ? Rustの ? を参考にした提案。コンパイラにプロトタイプ実装まで行われた。

    小規模なユーザー調査では大多数が意味を正しく推測できた。 → それでも再び大量のコメントが殺到。コンセンサスに至らず。 ref: https://github.com/golang/go/issues/71203
  19. エラーハンドリング構文の断念( 2025/6) 2025年6月、Go公式ブログでの幾度かの試みを経て構文変更を断念したことが公表された。 For the foreseeable future, the Go team

    will stop pursuing syntactic language changes for error handling. We will also close, without further investigation, all open and new proposals that are limited to syntactic changes to error handling. どの提案もGoチーム内部を含めてコンセンサスに達しなかった。 if err != nil は残り続ける。 → エラーハンドリングの構文すら変えられないなら、nilの意味論を変えるコストはそれ以 上。。 ref: https://go.dev/blog/error-syntax
  20. その他の提案 issue 内容 結果 #27890 nilメソッドレシーバを禁止してtyped nilを防ぐ 却下 #30294 nil

    receiverのinterface化に明示的キャストを要求 却下 #60786 interface == nil を値だけで比較するように変更 却下 #62487 nilをtype parameterで使えるようにする 却下 #56364 NaN (Not a Nil) 却下 #70367 「Make nil nil again」 却下
  21. なぜどれも採用されないのか Go 1互換性の保証 Go 1 compatibility promiseは正式な契約。Russ Coxは2023年に「Go 2でも過去のコードを壊すことはな い」と明言した。

    どの提案も「問題の移動」で終わる 例えば、nilptr/nilinterfaceは識別子が6つ増え、nullはnilとnullの使い分けという新しい混乱を生む。問題 を解決するのではなく、別の場所に移すだけになる。 Goチーム内部でもコンセンサスに至らない try()は900近いコメントが殺到。?演算子もプロトタイプ実装まで行って断念。「Goチーム内の最もシニアな メンバーでさえ合意に至っていない」 同じ処理を書く方法を複数用意しない 新構文を入れると、それを使わない既存コードが「非イディオマティック」とされ、別の不満が生まれるだ け。 経験者にとっては一度覚えれば対処できる Goユーザーの多くが「慣れれば問題は小さくなる」と回答。
  22. Ian Lance Taylorの言葉(2025) Go gets audited, and Ian Lance Taylor

    talks about 19 years on the Go team (Cup o' Go, Episode 111)にて、 The interface can hold any value. It can hold any pointer. It can hold a nil pointer. And that's — you know, we didn't find that confusing, but it's clear that many people do. I wish we had found a different way forward for that even if I don't know what that way is. そして、 At this point in the language, there's probably no answer. → interface は nil ポインタも含めて何でも保持できる。Go チームは当初それを混乱とは思わなかった が、多くの人が混乱しているのは事実。別の設計にできればよかったが、どうすればよかったかはわか らない。そして今の言語の段階では、おそらく解決策はない、という認識。 ref: https://share.transistor.fm/s/9cae9b8d The Go gopher was designed by Renée French.
  23. `errors.AsType`(Go 1.26) errors.As は典型的にnil変数を事前宣言して使う必要があった。スコープ漏れや typed nil混入のリ スクがあった。 // before: nil変数の事前宣言が必要

    var target *MyError if errors.As(err, &target) { ... } // after: AsType で宣言不要、スコープも閉じる if myErr, ok := errors.AsType[*MyError](err); ok { ... } nilの仕様は変えられなかったが、APIレベルでtyped nilに触れる機会を減らした。 公式ドキュメントでも As より AsType の使用を推奨している。 ref: https://pkg.go.dev/errors#AsType ref: https://github.com/golang/go/issues/51945
  24. パターン① : 独自エラー型の落とし穴 第2部で見た通り、nilポインタを error interfaceに入れると型情報が残るため err != nil がtrue

    になる。 実務ではORMやHTTPクライアントのラッパーで踏みやすい。 func findUser(id string) (*User, error) { var appErr *AppError user, err := db.Find(id) if err != nil { appErr = &AppError{err} } return user, appErr // ← appErrがnilでもerror interfaceはnon-nil } → 戻り値が error interfaceなら、具象型の変数を経由せず return nil と明示的に書く。
  25. パターン② : errors.As と typed nil パターン①の問題は errors.As でさらに深くなる。 func

    getFile() error { var pathErr *fs.PathError // pathErrを設定し忘れた場合 ... return pathErr // typed nil → non-nil error } func caller() { err := getFile() var pathErr *fs.PathError if errors.As(err, &pathErr) { // 型が一致するので true を返す // しかし pathErr は (*fs.PathError)(nil) fmt.Println(pathErr.Path) // panic! } } 第3部で見た errors.AsType はこの問題を軽減する。typed nilの変数を事前宣言しないことで、そも そもこの状況を作りにくくする。
  26. パターン③ : nil channelの活用 また、nilを設計に活かすこともできる。 nil channelへのsend/recvは永久にブロックする。これは select で使える。 for

    ch1 != nil || ch2 != nil { select { case v, ok := <-ch1: if !ok { ch1 = nil; continue } // ... case v, ok := <-ch2: if !ok { ch2 = nil; continue } // ... } } 使い終わったchannelをnilにすると、そのcaseはselectで選ばれなくなる(永久ブロッ ク=評価対象外)。 Dave Cheney「Channel Axioms」 https://dave.cheney.net/2014/03/19/channel-axioms
  27. なぜサードパーティに頼るのか golang.org/x/tools には nilness アナライザが存在するが、go vet には組み込まれてい ない。 SSA依存によるメモリ+32%・実行時間+33%のコスト増を理由に、追加提案は却下された (#59714)。

    typed nilのinterface変換を検出する提案(#69645)も調査段階で、コーパス評価では偽 陽性が多く実装に至っていない。 Goチームは精度(precision)を再現率(recall)より重視する方針で、偽陽性を出すくら いなら見逃す方を選ぶ。 → そのため go vet でtyped nil問題が検出されるようになる見込みは当面なさそう。。 → 現時点ではサードパーティツールが実質的に唯一の防御線
  28. まとめ • nilは1つの識別子だが型で実体が違う。interfaceの (型情報, データ) 構造が「nil != nil」の原因で、仕様通りの動作。 • 明示的な

    return nilが重要。errors.As とtyped nilへの注意が必要。 静的解析ツールでバグを防ぐ。 • 改善提案は15以上あるが後方互換性と設計思想で多くが不採用。 Ian Lance Taylorによると「おそらく答えはない」。 • 「nilでハマった」で終わらせず、Goがnilとどう向き合ってきたか知る と、言語の設計判断が見えてくる。
  29. 参考資料 • The Go Programming Language Specification https://go.dev/ref/spec • Go

    FAQ: Why is my nil error value not equal to nil? https://go.dev/doc/faq#nil_error • Russ Cox「Go Data Structures: Interfaces」(2009) https://research.swtch.com/interfaces • runtime/runtime2.go https://go.dev/src/runtime/runtime2.go • internal/abi/iface.go https://go.dev/src/internal/abi/iface.go • Francesc Campoy「Understanding Nil」GopherCon 2016 https://speakerdeck.com/campoy/understanding-nil • Ian Lance Taylor on Cup o' Go Episode 111 https://share.transistor.fm/s/9cae9b8d • Dave Cheney「Channel Axioms」 https://dave.cheney.net/2014/03/19/channel-axioms • Go Wiki: Code Review Comments https://go.dev/wiki/CodeReviewComments • Go 101: nils in Go https://go101.org/article/nil.html