🦜

GoのAPIが厳格でない訳

2021/08/04に公開

Windows対応の曖昧なAPIを非難する記事

https://fasterthanli.me/articles/i-want-off-mr-golangs-wild-ride

この記事はGoが曖昧に扱うAPIについて非難していて、より厳格に扱うことのメリットを解説しています。

Goのこれらの指摘の挙動が実際にどの様なものかを解説していきます。

無視する挙動

  • Goの標準ライブラリのAPIはどちらかというとUnix/Posixに寄せていて、一部のWindowsに無い概念に関する処理(ファイルのパーミッション操作など)は黙って無視したりする。

これはUnix/Posix用の実装が同じソースコードのままWindowsでも動作するために必要なダミーです。ここでそのようなダミー実装をアプリケーション作成側の責任にすると実装やテストが大変面倒になってしまう。

逆に、GoではUnix/Posixにあるforkやthreadに関するAPIをサポートしません。特にforkというAPIはWindowsには全くない概念であり、互換性を取るのが困難なためです。つまり、お互いにサポートしない機能は無視したり非推奨としてサポートしないという形を採ります。そしてforkやネイティブスレッドを使う実装の多くはgoroutineで代替実装できます。

パスセパレータの扱いがあいまい

  • os.IsSeparatorが「\」も「/」もtrueを返す

これはWindowsAPIがその様に受け取るからであり、何も問題はありません。
filepathパッケージはパスの構築は厳格に行うし、パスの解釈は曖昧に行う。
これは実利優先実装では非常によく採られる手法です(特にネットワークプロトコル実装分野で)。

InvalidなUnicodeのコードを置き換える挙動

  • GoからWindowsAPIを呼ぶとき、文字列パラメータはUTF-8->UTF-16変換される
  • 戻り値が文字列の場合、UTF-16->UTF-8変換されます。
  • その際INVALIDなUTF-8コードやUTF-16のペアでないサロゲートをUnicode標準規約に則り「はてなマーク」に置換する。
  • UTF-16を格納するためにUInt16型を利用している処理系により作られた実装が「INVALIDなUTF-16で構成される文字列データ」を投げ込んだとき、WindowsAPIはUTF-16の無効なコードも最低限のASCII文字チェックのみで受け入れてしまう。
  • WindowsAPIが受け入れてしまう上記の様な文字列はGoからはどうやっても渡せないことになる。
  • RustではWTF-16/WTF-8というエンコードを規定し、WindowsAPIで渡したり受け取ったりするwcharのとりうるコード範囲全て(InvalidなUnicodeも含む)をそのまま保持可能にしている。

これはユーザーの立場に立って考えると一般のアプリケーションではGoの挙動の方が望ましい。(可視化できない差を持つファイル名が作られた場合、ユーザーによる対処は困難。)そしてGoと同様の挙動を行うアプリケーションのみになればGoの挙動が厄介となる状況は再現されなくなる。(PythonアプリケーションもValidなUnicodeしかWindowsAPIに渡せない)

InvalidなUnicodeで構成されたファイル名のファイルを一般アプリケーションで開こうとしたときにエラーになったりすることは起こるでしょう。その様なファイルは検査で発見してアクセス可能なファイル名に修正するツールなどを利用する必要があります。そういうツール(つまりファイルシステムの検査ツールのようなもの)を作るのにはGoは向かないということは言えますが、一般用途ではGoの挙動の方が困ることは少ないはずです。

./.fooの拡張子は「.foo」とする挙動

  • Windowsでは先頭がドットで始まるフレーズを拡張子とは扱わないことを正確に扱いたい?

Unix/Linuxでは拡張子という概念はOS上で特別扱いしないので、「拡張子」は「最後のドット以降」という意味で扱います。この件はWindows特有のルールなので特有のルールをコードの書き手に認識させるよりはシンプルなルールで拡張子を扱う方が誤解を生まないという判断だと思います。

プラットフォーム別の記述を同じファイルに書けない

  • プラットフォーム別の記述がインラインで書けるほうが嬉しい?

クロスプラットフォーム対応のC/C++ライブラリソースを読んだことがあればわかってもらえると思いますが、これは書けてしまうとifdefマクロ祭りと同じく処理フローが追いにくくなります。

Goの様にプラットフォーム別のファイルごとにコードの流れが追える方が読みやすいです(記事の指摘通り確かにコードは若干冗長にはなってしまいますが)。

モノトニック時刻の扱い

これはレアケースで問題があったものを塞いだということであり、新しいバージョンのGoに切り替えることでもう問題では無いはずです。(また別件でPCスリープ時にモノトニック時刻とリアル時刻に差分が生まれる問題が発生していますが)

idletimingパッケージの多すぎる依存

やたらと丁寧に依存パッケージが多すぎることを非難していますが、これはこのパッケージの問題であって、Go本体の問題ではありません。また、すでに同等の機能が標準のhttpパッケージに実装されたのでこのパッケージを利用する理由はもうありません。

64bitsアライメントを跨ぐatomicによる64bitsアクセスが32bitsアーキテクチャでのみランタイムパニックになる

かなりレアケースなんですが、コードをRaspberryPiなどに持っていくときなどに発覚します。これは静的解析で検出する機能が利用可能です。

https://gist.github.com/nobonobo/167580ce4343ee48751ea37772816175

まとめ

  • goroutineにしろfilepathパッケージにしろ極力同じコードが異なる環境でも動作する様に調整されています。
  • これらの「曖昧さ」はGoが「クロスプラットフォームで同一のソースコードが動く」様に工夫している事の表れです。
  • 逆にプラットフォームごとに厳密なAPIを標準に持たせてしまうと、ライブラリ資産の分断が発生してしまう(環境依存APIを呼んでしまうライブラリは他の環境では使えないライブラリになる)。
  • ファイルロックAPIやプロセスフォーク、スレッド周りのAPIなどがGoの標準で提供されていないことはライブラリの分断を回避するのに貢献しているのです。
  • 例えば、Python/Linuxの便利なライブラリをWindowsに移植しようとしたらプロセスフォークを使っていてその部分の書き直しが結構大変だった思い出があります。
  • 各種OSの細かいAPI機能が使えない代わりに、そのおかげで公開されているライブラリやアプリケーションの多くはOS環境依存が少なく、任意のOS環境で利用可能なのです。
  • 元記事は非常に丁寧に困っている事を解説してくれてはいるのですが、「より厳格であるべき」が先行しすぎて「曖昧であることのメリット」が語られていなかったので補足しました。

Discussion