なぜGo言語は設計が悪いのか – Go愛好者の見地から

さて、このタイトル、かなり挑発的ですよね。それは認めます。もう少し説明すると、私は大胆なタイトルが好きなのです。人の注意を引くことができますからね。とにかく、この記事では、Goがひどい設計の言語(実際、本当に全て台無しになります)だということを証明していこうと思います。私は既に数カ月間Goで遊んでいますし、たしか6月のいつだったかに初めてHello, Worldを走らせてもみました。私は数学がそんなに得意ではありませんが、あれから既に4カ月経っていますし、Github上のパッケージもいくつか手に入れました。言うまでもありませんが、私は仕事でGoを使ったことは全くないので、”コードサポート”や”デプロイ”やそのあたりに関する私の意見は話半分で読んでくださいね。

私はGoが大好きです。使ってみて大好きになりました。慣用表現を理解したり、ジェネリクスがないことや、おかしなエラーハンドリングや、その他全ての昔ながらのGoの課題を解決したりするのに2、3日かかりました。私はEffective GoやDave Cheneyのブログをたくさん読み、Goに関することには常に注意を払っていました。私は現役のコミュニティメンバーだと言えるでしょう。Goに対する愛を抑えることはできません。Goは素晴らしいのです。それでも、私の個人的な意見を言わせてもらうと、Goはひどくぞんざいなデザインの言語で、うたい文句とは全く逆のことをするのです。

Goはシンプルなプログラミング言語だとされています。Rob Pikeいわく、彼らはこの言語から全てを取り払い、スペックを単純なものにしたそうです。この側面は素晴らしいと思います。基本を数時間で習得でき、すぐにコーディングに入れて、きちんとそれが機能するということであり、Goは大抵の場合、期待どおりに機能してくれます。時にイライラするかもしれませんが、あわよくば機能します。現実は彼の話とはだいぶ違い、Goはシンプルな言語なのではなく、ただお粗末なのです。これを証明するポイントをいくつか挙げていきます。

理由1. スライス操作がおかしい!

スライスは素晴らしいもので、私はこの概念といくつかの実装が大変好きです。しかし実際にGoで何かのソースコードを書きたくなるかもしれないということを少し考えてみましょう。明らかに、スライスはこの言語の核となる部分にあり、Goを素晴らしいものにしています。しかしやはり、時として”概念”の話のあいまに、実際にコードを書きたくなるかもしれませんよね。以下のコードのリストはGoでのスライス操作のやり方です。


信じられないかもしれませんが、Goのプログラマは毎日このようにしてスライスを変換しています。そして、Goにはジェネリクスのようなものがないので、この恐ろしいものを隠すinsert()関数を作れないのです。これをplaygroundに投稿しましたので、皆さん私の言っていることを信じるのではなく、自分の目で確かめてください。

理由2. nilのインターフェースは必ずしもnilではない

“Goのエラーは文字列以上”であり、エラーを文字列として扱うべきではないと開発者たちは言っています。例えばDockerのspf13も、”Goでよくある7つの間違いとそれを避けるべき時”という非常によい講演の中で同じことを言っています。

また、いつでもerrorインターフェース型(一貫性、可読性、他)を返すべきだとも言っています。以下のコードリストは、これを行ったものです。驚かれるかもしれませんが、このプログラムでは確かにHello, Mr. Pikeと出力されます。しかし本当にそうなると予想できますか?

ええ、私にはこうなる理由が分かっています。インターフェースのことや、Goでのインターフェースの動き方について、包括的なリソースをたくさん読みましたからね。しかし初心者にとっては…キツイです! 実際、これは陥りやすい落とし穴1つです。ご覧のとおり、Goは邪魔な機能を取り払った、分かりやすく簡単に学べる言語ですが、時々nilインターフェースがnilじゃないということが起こるんです。

理由3. 面白い変数のシャドーイング

この用語をよく知らない方がいた場合のために、Wikipediaからの引用を載せておきます。”変数のシャドーイングは、特定のスコープ(決定ブロックやメソッド、内部クラス)内で宣言された変数が外側のスコープで宣言された変数と同じ名前を持つ時に起こる。”この説明は筋が通っていて、とても常識的に思えます。大抵の言語は変数のシャドーイングをサポートしていて、それは全く問題ありません。Goも例外ではないのですが、他の言語とは違うのです。以下がシャドーイングの動き方です。

ええ、分かっています。:=演算子が新しい変数を作成してそこに右辺値を割り当てるんですよね。だから言語スペックによれば、これは完全に筋の通った振る舞いです。しかし面白いことがあるのです。内部スコープを取り除いてみると、ちゃんと予想(”after 42″)どおりに動くでしょう。でなければ、変数のシャドーイングが起こります。

言うまでもなく、これは単に私がランチの最中に思いついた面白い例ではありません。最終的に皆さんが本当に出くわすものです。私は今週初めからGoのコードをリファクタリングしているのですが、これに2回もぶち当たりました。コンパイラは問題なし、linterも大丈夫、全て問題ないのに、コードが機能しないのです。

理由4. []structを[]interfaceとして渡せない

インターフェースが素晴らしい。Pike &Co.はずっと、それこそがGoだと言い続けています。インターフェースはジェネリクスのワークアラウンドのための方法であり、モックテストを行う手段であり、ポリモーフィズムを実装させる方法です。本当に、”Effective Go”を読んでいる間は、私は心からインターフェースが大好きでしたし、今も好きです。しかし上記で述べた”このnilインターフェースはnilじゃない”問題以外にも、Goではインターフェースが第1級のサポートを持たないと思わせる扱いにくい点があるのです。基本的に、このインターフェース型のスライスを受け取り構造体のスライス(一部のインターフェースを満たすもの)を関数に渡すということができません。

当然、これはよく知られていることで、問題だとは全く考えられていません。Goの、単にまた別の面白い特徴に過ぎないということですよね。このことに関するwikiのサイトをぜひ読んでください。そうすれば、なぜ”構造体のスライスをinterfaceのスライスとして渡すこと”がうまく機能しないのかが分かるでしょう。でも、ちょっと考えてみましょう。魔法などではなく、解決可能です。これは単にコンパイラの問題です。49行目から57行目では、[]structから[]interfaceへ明示的変換を行いました。なぜコンパイラがやってくれないのでしょうか? 確かに、暗黙より明示的な方が良いですが、一体それが何だっていうのでしょう。

私が我慢できないのは、言語が抱えるこうした類の厄介ごとに対して「それはそうだけど、別にいいじゃないか」と言い続ける人が実に多いということです。そのままでいいわけがありません。そういう態度が、Goという言語をダメにしているのです。

理由5. “by-value”ループの非自明のrange

これは私が言語に関して初めて経験する問題です。確かに、Goには”for-range”ループがあります。これは、スライスをレンジし、チャネルを読み込むためにあります。様々な場所で使われ、それについては何の問題もありません。ただし、未だに多くの初心者が常につまずく、ちょっとした問題があります。それはrangeループはby-valueだけに限られており、値をコピーするだけで何もできず、C++のforeachとは全く違うということです。

Goにby-reference rangeが無いと不平を言っているわけではなく、rangeが非自明であることに対して不平を言っているのです。”range”という動詞は、”アイテムを繰り返す”というような意味であり、確かに”アイテムのコピーを繰り返す”という意味ではありません。”Effective Go”の Forの項目を見てみましょう。”rangeはスライスから値をコピーする”というようなことは書いてありませんし、実際にそうではありません。これがささいな問題であることは認めますし、私はこの問題をあっという間に(数分で)解決しました。しかし経験の少ない人ならば、なぜ値が変わらないのか悩んで、コードのデバッグに時間がかかってしまうかもしれません。せめて、その点に関して”Effective Go”で詳しく説明できたはずです。

理由6. コンパイラの柔軟性の欠如は、はなはだ疑問である

前に述べたかもしれませんが、Goは厳格なコンパイラを備えた、明快かつ簡潔で読みやすい言語です。例えば、unused importではプログラムをコンパイルできません。なぜでしょうか? それは単に、Pike氏が、それが正しいと考えたからです。信じないかもしれませんが、unused importは世界の終わりではありません。私はunused importとうまく共存しています。それは正しいとは言えないし、コンパイラは関連する警告を出力するべきだという意見には大賛成です。でも、そんなささいなことで、なぜコンパイルを停止させなければならないのでしょうか。unused importだからという、本当にそんな理由で?

Go1.5は、面白い言語仕様の変更を行いました。この変更により、含まれている型名を明示的にリスト化せずに、マップリテラルのリストを作成できます。明示的な型のリスト化は余計かもしれないと気が付くのに、5年(もしくはそれ以上)かかったのです。

Goの特に使いやすい点をもう1つ挙げると、それは可読性、つまりコンマに関することです。Goでは、inportやconstやvarのブロックを複数行に渡って自由に定義できます。

import (
    "fmt"
    "math"
    "github.com/some_guy/fancy"
)
const (
    One int = iota
    Two
    Three
)
var (
    VarName int = 35
)

確かにとてもいいですね。しかし、”可読性”の点を考えた時に、Rob Pikeはコンマの追加が必要であると決めました。そしてコンマの追加後、ある時点で彼は、他の部分でもコンマを同様に使い続けるべきだと決めたのです。ですから、次のように記述するのではなく、

numbers := []Object{
    Object{"bla bla", 42}
    Object("hahauha", 69}
}

以下のように記述しなければなりません。

numbers := []Object{
    Object{"bla bla", 42},
    Object("hahauha", 69},
}

Listsやmapsではコンマを省略できないのに、なぜimportやvarやconstのブロックではコンマを省略できるのかは、いまだに分かりません。いずれにせよ、Rob Pikeは私より分かっているわけです。可読性バンザイ!

理由7. Goの生成はとても風変りである

最初に言っておきますが、私はコードの生成について文句があるわけではありません。Goのような乏しい言語にとっては、ジェネリクスのようなことをするために、コピー&ペーストを省く唯一の実行可能な方法かもしれません。それでもやはり、世界中のGo利用者が使っているコードの生成ツール、go:generateは、ただのゴミです。公平な言い方をすれば、ツールそれ自体には問題ありません。私も気に入っています。しかし、アプローチ全体が間違っています。それは何かというと、コードを生成するために、*特殊な魔法のコメント*を使うのです。そう、コードのコメント内にある数バイトの魔法の文字列が、コードを生成してくれるのです。

コメントはコードを説明するものであって、コードを生成するものではありません。しかしそれでも、魔法のコメントは今もGoに存在しています。面白いことに、気にする人は誰もいません。*それでいい*と言います。余計なお世話かもしれませんが、言わせてもらえば、例のとんでもないunused importsよりもさらにひどい代物です。

最後に

お分かりのように、私は、ジェネリクスやエラーハンドリング、糖衣構文やその他、Goに関して昔からある問題に対して文句を述べたのではありません。ジェネリクスは致命的な問題ではないという意見には賛成ですが、もしジェネリクスを無くすなら、ランダムでヘンテコな魔法のコメントではなく、普通のコード生成ツールがほしいのです。もし例外処理を無くすなら、インターフェースをnilと安全に比較できるようにしてほしいのです。もし糖衣構文を導入しないなら、「おっと、びっくり」な変数のシャドーイングではなく、予想通りに機能するコードを書かせてほしいのです。

そうは言っても、私はGoを使い続けるでしょう。それには十分な理由があります。Goをとても気に入っているからです。確かにGo言語にはうんざりします。まるでポンコツですが、一方で、そのコミュニティや周辺ツール、素晴らしいデザインの決定(インターフェースとか)やGoのエコシステム全体が大好きなのです。

さあ、あなたもGoに挑戦してみませんか?