🦜

Goのヌル安全について

2020/10/26に公開
3

「ヌル参照の考案は10億ドル単位の過ち」と語ったホーア氏(Goの並列処理モデルCSPの考案者でもあります)。そしてモダンな言語処理系は「ヌル安全」を持つのが流行です。しかし、Goには完全な「ヌル安全」の仕組みを持ちません。

Goのメモリ安全機能

もちろんGoは完全なヌル安全とは言えないまでもヌルポ参照対策や不正なメモリ参照を防ぐいくつか考慮した仕組みや慣習を持っています。

  • ポインタの算術移動を許さない言語仕様
  • 確保するメモリは全てゼロ値で初期化済み
  • エラーがnilなら有効な値を返すという慣習
  • 必須のエラーチェックがヌルチェックを兼ねている

これらによりGoは完全に「メモリ安全」であり、「ヌル安全まであと一歩」までの仕組みを持っています。それでもヌルポ参照は「ランタイムパニック」という形で現れます。

ランタイムパニック

Goでは「ランタイムパニック=コードの不備の通知」です。

多くのコードの不備はコンパイル(パース)時に捕捉し、その他のコードの不備を起動時やテストのタイミングで捕捉するというのが高品質なプログラム開発の基本です。前者と後者の比率が言語処理系ごとに異なるだけなのです。

  • 動的型言語処理系ではおおまかなシンタックスエラーだけをパース時に捕捉、その他をランタイムにて捕捉します。
  • 強力な型システムをもつ静的型言語処理系では多くの問題をコンパイル時に捕捉します。
  • Goはどちらかというとその中間くらいのデザインであるということです。

その他のコードの不備

その中、ヌルポ参照以外にも、以下のような他のコードの不備の捕捉作業は残ります。

  • 静的なテンプレートがコンパイルエラー
  • 静的な正規表現がコンパイルエラー
  • 整数に対するゼロによる除算
  • スライスや配列のインデックス範囲外アクセス
  • スタックオーバーフロー
  • メモリの枯渇

上記はランタイムパニックという形で捕捉できますが、
実行時に捕捉が必要なコードの不備はそれだけではなくて

  • float32/float64の極やピンポイント比較の不備
  • 数値型のオーバーフロー・アンダーフロー
  • メモリーリーク
  • リソースリーク
  • goroutineリーク
  • デッドロック
  • データレース(競合)

以上の作業に加えて「ヌルポインタの参照」の捕捉が必要というだけなのです。

トレードオフ

これを言語仕様で縛ってコードの書き手が上乗せした情報をもとにコンパイル時に不備の捕捉を保証しようという考え方がヌル安全の考え方ですが、これは多種にわたるプログラム品質担保作業のほんの一部でしかないわけです。

Goでは言語仕様を無闇に増やすことになるヌル安全よりも言語仕様の単純さや安定を優先し、
エラーチェック忘れの検出やデッドロックやデータレースの検出を支援するツールの充実を優先しました。

これは開発を回せばわかりますがどっちのやり方が良いとか悪いなんてことはありません。
どちらにもメリットやデメリットがあるトレードオフな問題です。

実体験

自分の実際の体験でいうと確かにGo書き初めの段階ではちょくちょくヌルポ参照パニックに遭遇していましたが、近年は変数束縛時にポインタ(参照)型の値の初期化を心がけたり、ツールの支援で警告を修正することで実働環境でのヌルポ参照はほぼ遭遇しなくなりました。要するに理解して書いている場合、ヌル安全は十分確保できるようになったということです。

「理解するまでのミスやポカをなくせるのがヌル安全の素晴らしさである」というのはもちろん同意するところなのですが、どの道、品質を高めるにあたって起動やテストによる問題の捕捉作業の多くが減るわけではありません。であれば、無理してコンパイル時チェックに載せられなくても静的解析ツールで捕捉、起動やテストで捕捉できれば十分であるという考え方もできます。

理解して使ってくれ

例えば「typed-nilを格納したinterface型はnil判定できない」というものがありますが、
コアメンバーの一人からは「==nilで比較可能にするほうが初心者の誤解を招かないと思う」という主張もありましたが、最終的には「理解して使ってくれ」に落ち着きました。

このようなスタンスはいろんなところで観測できます。

Goコアメンバーのコメントから読み取れるのはあちこちに「理解して使ってくれ」「理解しやすさのためだけに言語仕様を変えたくない」、「理解が必要なところはドキュメントでカバーする」という考え方が見え隠れしています。ここがGoがプログラミング初心者には辛いかのように見えるところですが、これらのドキュメントの多くは数分で読めるコンパクトなドキュメントばかりです。

Goのテストや静的解析ツール

幸い、Goはテストを書いたり静的解析ツールを作ったりがとても容易です。標準に近い形でそれらのための枠組みを持っています。しかもGoにおける静的解析ツールの作りやすさは型システムのシンプルさなどもあいまってかなり突出しており、比肩できるような他の処理系はおそらく存在しません。そして、Go1の約束があるため長期間大きく言語仕様が変更されることもありません。(Go1.0から9年で軽微な変更1〜2点くらいしかありません)

そのせいか、静的解析ツールの充実具合も品質もトップクラスです。

また、型の強力な言語の静的解析ツールを作ってみれば実感できますが、型システムを利用するだけにくらべ型システムの上にツールを作る場合はさらに型システムへの詳細な理解が要求されます。検査したい対象にほんのちょっと型修飾が増えるだけで検査ツールを作るためのコストが大幅に膨れ上がりますし、2~3年にいくつか言語仕様に変更が入ってその検査ツールのメンテが滞ったりしちゃうと利用不可能になったりします。つまり多くのユーザーが必須と思えないツールは誰かがしつこくメンテし続けないとどんどん使えなくなります。

これまでの言語処理系では信頼できる静的解析ツールはよほどマンパワーが集中したところでないと維持できなかった(C++とか馬鹿高いライセンスを払ってもなお不満が残るようなツールが多かった)ので、いろんな保証を持つ場所は「コンパイラ」でなければ信頼できなかったというのもあるかと思います。

しかし、Goは違います。静的解析ツールの信頼性が高い上に改良も多くのGopherがチャレンジできるほど情報や支援ライブラリが充実しています。

recoverについて

  • 基本recoverは使わないようにしよう。
  • 安心のためにrecoverを仕掛けるのは「理解して書く」とだんだん不要だとわかってきます。
  • プログラムが想定した条件を満たしている状況でpanicを上げるようなライブラリをGopherは使いませんし作りません。
  • panicが上がるとしたら使い方を間違っているか実装にミスがあるか、致命的な何かが起こったということなのでそういう時は素直にプロセスを落とす方が状況を悪化させないことが多いです。
  • プログラムの品質を上げるにはpanicが出た状況をよく確認してその要因をきっちり取り除くことが重要です。

まとめ

  • Goにはヌル安全を支援する機能が全くないわけではありません。
  • エラーチェックは必須だけど、それが戻り値のヌルチェックを兼ねています。
  • コンパイル時よりも起動やテスト、静的解析ツールを使ってヌル安全を確保するのがGoの流儀です。

Discussion

名前迷子名前迷子

大変素晴らしい内容の記事を公開していただきありがとうございます。
他の言語から移ってきて、Go の初心者の私としては大変参考になる内容でした。
優れた静的解析ツールというのがとても気になりました。どのようなツールを愛用されたり代表されるかなど、できれば教えて頂きたいのですがお願いできますでしょうか。

NoboNoboNoboNobo

例えば、 https://github.com/kisielk/errcheck でエラーチェック漏れを検出できます。
他の記事にも書いていますが、「ptr, err := func1()」にてエラーチェックのあとはptrがヌルではないことが保証されます(実装者がそう保証すべきという慣習があります)。

また、TypedNil問題の検出には以下の様なツールが使えるかと思います。
https://github.com/makiuchi-d/ptrtoerr
https://github.com/gostaticanalysis/typednil

https://github.com/gostaticanalysis/ には優れた静的解析ツールの雛形生成からフレームワーク、よく使うツールなどのリポジトリ群があります。ここのコードやドキュメントに学ぶことで必要な検査を含む静的解析ツールを自作するのも利用するのも容易です。

名前迷子名前迷子

ご返信ありがとうございます。
ツールについても紹介していただきありがとうございます
大変参考になりました