ある仕事でそれまでRubyで書かれていたサーバーサイドをGo言語ですべて書き直すことになって、それまでRubyのコードを書いていた僕はそのままGo言語を書くことになった。その仕事そのものはお客様(僕は外部委託のエンジニアとして参画していた)との関係も良好で素晴らしい仕事をさせてもらうことができたと思っているが、Go言語だけは好きになれなかった。 はじめは流行っている言語だから何か素晴らしい魅力があるのではないかと期待していた。しかし書き始めるうちにどうも自分には合わないなと思うようになり、2年ほど書いて案件の契約が終わる頃にはGo言語でサーバーサイドを書くことは危険だとさえ思うようになった。
あれから数年がたちますますGo言語の案件は増えている。サーバーサイドを書く選択肢としてGo言語を選択する会社も増えている。しかし本当にそれでいいのか?ただ流行っているからという理由だけで選択するにはあまりにもGo言語はやっかいなものではないかと思う。僕の知人がGo言語でなにかやろうとするたびに僕はかならず「それは愚かな選択ですね」とか「それは間違った文明ですね」などと声をかけるようにしているので最近はすこし顰蹙(ひんしゅく)を買っている。それほどに僕はGo言語が嫌いだし安易に選択する技術ではないと思っている。
Go言語をシンプルな言語だと言う人もいる。まぁわからなくもない。構文や機能が少ないことをシンプルと呼ぶならそうだろう。だが僕に言わせればGo言語はシンプルとかEasyとかいうより単に機能が足りてないだけだ。
Go言語を選ぶ特別な理由がない限りGo言語でサーバーサイドを書くべきではない。特別な理由というのはつまりGoルーチンを使いたいとかGo言語のデメリットを加味してもGo言語のスピードとか静的型付けなどのメリットを活かしたいという場合だけで、実のところそのようなケースは稀だと思っている。
ではもう少し具体的に僕がGo言語を嫌う理由を説明しよう。
map, reduce のような機能がない
Go言語を嫌う多くの理由の中で一番はやはりこれかなと思う。僕はClojureやRubyのような関数型の影響が強い言語に慣れているからどうしても配列操作はmap, reduce のような関数で行いたい。しかし Go 言語にはそのような機能はなく、とにかく手続きを for 文で書いていくしかない。これが不便だと感じている Go 言語ユーザーはそれなりにいると思うし、比較的批判が少ない部分ではないかと思う。
たとえば、ある配列のなかから条件を満たす要素だけを取り出したいときに、関数型言語であれば filter を使えばすむようなところを Go 言語は for 文で10行くらい書かなればならない。
コードの記述量が多いことによりメンテナンス性が下がる
map, reduce のような配列操作系の関数がないことなどの理由によりGo言語ではひたすら自分たちで似たような処理をゴリゴリ書いていく必要がある。すぐにファイルの長さは1,000行を超え、10,000行くらいあるファイルも珍しくない。 これは単純だがかなり厄介な問題でファイルが大きくなればなるほどバグも増えるし仕様の把握も難しくなるし、コードを読むのが大変になる。つまりメンテナンス性が下がる。
またコードを書くのも大変になる。先程のfilterの例のように同じような処理をたくさんかくのでだんだんパターンが分かってきて早くかけるようにはなるのだが、それでも記述量が多いことに変わりはないし人間のタイピングスピードなどたかが知れているので時間もかかる。昨今はエディタの保管機能がとても優秀だがそれでも保管したコードが間違っていないかどうか確認する必要があるのでコードを書く速度はあまり変わらない気がする。
人手が必要で人件費がかかる
コードの読み書きが大変ということはそれだけ人手がいる。僕の感覚としてはRubyエンジニアが1人いれば十分だったプロジェクトをGo言語に置き換えると3人はエンジニアが必要だと感じている。それだけの人件費を払うことができるだろうか?特にスタートアップなどのベンチャー企業で。
ある程度規模の大きい会社で人海戦術的な作戦もとれるような会社であればGo言語を採用してもいいだろうが、とにかく人件費を削減する必要のあるスタートアップでGo言語を採用するのはそれだけでかなり危険だ。ましてや世の中に優秀なエンジニアは少ないわけで、スタートアップで3人の優秀なGo言語エンジニアを採用することは現実的ではないとさえ思う。
Error処理がお粗末
コードの話に戻ろう。Go言語の特徴的なことの一つとして例外をあまり使わないということがある。Go言語を書き始めて最初に驚いた部分でもあるが、Go言語においては実行時エラーを補足して処理を書くことはあまりなく、関数の戻り値の最後の値で Error を返すようにしておいて、それを呼び出し側の関数で if err != nil
のような条件分岐で処理することが一般的だ。この方法のメリットは静的なコンパイラで error をある程度捕捉できることで、これにより実行時エラーをかなりへらすことができる。
まぁ、メリットは認めよう。いいと思うよ。でもさすがにもう少し言語からのサポートがあってもいいんじゃないかと思う。例えば関数Aから関数Bを呼び出してさらに関数Bから関数Cを呼び出した場合に、関数Cでの Error を関数A で処理したいというケースを考えよう。これはサーバーサイドプログラムでコントローラ層、ドメイン層、データベース層のように階層構造をもつ設計をした場合によく発生するケースだ。この場合Go言語であれば関数Cの戻り値の Error を関数Bから関数Aにそのまま渡すことで処理するしかない。try catch のような例外処理構造であればいきなり関数Aで処理することができるが、Go言語ではしない。
Try catch を嫌うのはわからなくもない。部分的なGoto文のような機能だといえばそのとおりだと思う。しかしネストした errorを扱うための言語としてのサポートがもう少し何かあってもいいんじゃなかろうか。単に if 文で全部処理しろというのはさすがにお粗末すぎると思う。
nil を受け取りたい場合はポインタにする
Go言語で nil を変数で受け取りたい場合には型をポインタ型にする必要がある。 例えば関数の引数で String 型を受け取るときに nil が渡ってくる場合があるときには String 型を String ポインタ型にする必要がある。 nil は値ではない。メモリ中のアドレスを指し示す場所がない状態を nil と呼ぶのだ。Go言語の思想はそんなところだとおもうのだが、これもちょっと言語としてのサポートがお粗末なんじゃないかと思う。
nil (null) の扱いはどの言語でも難しいところだが、ポインタ型にすることで結局実行時例外(Null Pointer Exception)が発生する危険性を含んでいるし、String型で扱えればいいところがStringポインタ型になることで処理の書き方が変わって面倒くさい。これは特にそれまで nil を受け取らなかった部分で nil を受け取るように変更する場合に書き換えコストがかなり高い。
ポインタにするんじゃなくて nil を含む String 型を用意したほうが便利だったんじゃなかろうか。実際そういうライブラリを作っている ORM もある (プロジェクトではSQLBoilerを使っていた) わけで、 nil だからポインタにするというのはちょっと短絡的すぎるというかやりたいことに対して違和感がある。
JSON などの外部リソースを扱うときに構造体が必要
これは僕の知識不足かもしれないがGo言語でJSONなどをパースしてメモリに展開するときには事前にJSONの構造と同様の構造体を定義しておく必要がある。ネストした構造に対してはそれぞれに構造体を用意する必要があり、JSONが巨大になればそれだけGo言語中の構造体も複雑かつ巨大かつ複数(ネストした構造は別の構造体にする必要があるため)になっていく。
型がきちんとある方が良いという考えもあるだろうが、僕の考えとしてはJSONとは基本的に外部APIからのレスポンスであり非常に変わりやすいもので、それに対する振る舞いがストリクトなのはかえって無駄が多いように思う。自身のプログラムになんの欠点もなくても少し呼び出し先のAPIのレスポンスに変更があっただけで動かなくなるというのは本当によいプログラム呼べるのだろうか。 できればAPIレスポンスのうち必要な部分だけいい感じに扱えたほうがいいわけで、Go言語のやり方はそういう柔軟性がない。
Go言語は少しプログラムを静的なものとして捉えすぎているように思う。実際のプログラムは動的なもので自身のコード以外の、いわゆる外界とのやり取りが発生するもので、Go言語は外界とのやり取りがあまり上手な言語ではない。そういう意味でWebプログラミングのような外界とのやりとりが多いプログラムはGo言語には向いていない。僕の好きなClojureとはこの辺がまったく逆なのでかなり思想の違いを感じるし、僕のGo言語アレルギーの原因はまさにこの点である。 Clojureではプログラムを動的なものとして捉えすぎていて、静的な部分もあるよねみたいなところには疎いのだが、外界とのやり取りはClojureは他の言語の追従を許さないレベルで得意な言語だ。よってWebプログラミングではClojureが最強である。
追記: Tiwtterから突っ込みいただきました
興味深く読みました。ありがとうございます。
— ヽ(´・肉・`)ノ (@niku_name) 2021年9月16日
JSON などの外部リソースを扱うときに構造体が必要の項については、構造体を用意せずとも map[string]interface{} に書き出せると思います。
まとめ
他にもいくつかGo言語の嫌なところ(パッケージマネジメントがいまいちだったり、実行ファイルのサイズが巨大だったり)は色々とあるのだが、コードを書くをという点に注目して嫌悪している部分については以上となる。 Go言語をプロジェクトで使ってみようと思っている人が少しでも思いとどまって考え直すきっかけとなってくれたら幸いだ。 勘違いしないでほしいが僕は上記のようなデメリットを考慮した上でそれでもGo言語のメリットを活かすことができるのであればむしろGo言語を積極的に使っても良いと思っている(じゃあそういう案件があったら仕事を受けるかって?いや僕はやらないけどね…)。Goルーチンのように並列並行処理を扱いたいケースは増えてきているし、静的型付けの恩恵にあやかりたい気持ちもわかる。だがClojureなどの他の言語にも似たような機能は取り込まれて来ているし、Go言語の強みは必ずしもGo言語の専売特許ではないことを思い出してほしい。 僕の予想ではあと数年したらGo言語のデメリットにみんなうんざりして別の言語(たぶんdeno)をつかってプログラムを書くんじゃないかと思っている。