こんにちは、バックエンドエンジニアのKazです。
最近LIGでは自社サービスの開発を進めていて、そのいくつかのプロジェクトでバックエンドの開発に「Go」という言語を採用することになりました。
2009年に初登場し、最近ではさまざまな企業やオープンソースのプロジェクトで採用されることが増えてきたGo言語。
注目度合いが高まっている風潮もありますが、これまでのバックエンド開発では主にPHPを採用し続けてきた弊社がなぜ今回Goを採用するに至ったのでしょう?
この記事では、弊社がGo言語を採用するにあたった本当の理由のご紹介に先立ち、使ってみて実感したGo言語の良さを解説していきたいと思います。
目次
GoとPHPの違いは?
「Go」は2009年にGoogleによって発表された、比較的新しいプログラミング言語です。Goという名前はシンプルすぎて大変紛らわしいので、近年ではもっぱら「Go言語」「Golang」などと呼ばれます。
YouTubeを初めとしたさまざまなGoogleのサービスやDropbox、最近話題のUberなどでもサービスのクリティカルな部分にGo言語を採用していたり、近年インフラ界隈で注目されている「Docker」というオープンソースプロジェクトもGoで作られていたりと、日を追うごとにGo言語が採用されるケースが増えてきています。
Goの特徴をここで語るととても量が膨大になってしまうので、ここでは簡潔にWebのバックエンド開発でデファクト・スタンダードとなっているPHPと比較してその主な利点を説明していきます。
Go vs PHP: スピード対決
PHPと比較すると、Go言語はとても高速に実行されます。
以下に実際にスピードを比較したものを掲載します。比較方法はPHP 5.6とPHP 7.0、Go 1.6で10万回バブルソートを行うものです。公平性を期すためソースコードはこちらのサイトに掲載されているものを用い、真っ更なLinux仮想マシン上でテストを行いました。
回数 | PHP 5.6.22 (cli) | PHP 7.0.7 (cli) | go1.6.2 linux/amd64 |
---|---|---|---|
1回目 | 2142.990 ms | 781.001 ms | 27.965 ms |
2回目 | 2139.427 ms | 797.260 ms | 25.754 ms |
3回目 | 2155.695 ms | 798.832 ms | 24.993 ms |
平均 | 2146.037 ms | 792.364 ms | 26.237 ms |
この結果では、Go言語がPHP 7の30倍、PHP 5.6と比べるとなんと82倍にも及ぶスピードを示しています。(※あまりに差が大きすぎてGoのバーが見えなくなったので、グラフは対数にしています)
PHP 7では内部仕様を刷新し大幅な高速化を図り、かつてないほどの高速化を成し遂げました(このテストでは2.7倍の速度を叩き出しています)。これはPHP 5系最初のリリースから10年もの月日を経てようやく成し遂げられたものであり、通常ここまでの高速化は一筋縄ではいきません。では一体なぜ、GoはそのPHP 7よりさらに30倍もの速度を成し得たのでしょうか?
その理由の一つは、次に紹介するGoの「コンパイラ言語」という特徴によりもたらされています。
Go vs PHP: 実行のつど解析するか、先に変換しておくか
Go言語とPHPの最も大きな違いとして、Go言語は「コンパイラ言語」、PHPは「インタプリタ言語」(スクリプト言語)であるという点が挙げられます。
コンピューターは、実はGoやPHPといった「高級な」プログラミング言語をそのまま実行することができません。これらのプログラムを実行するためにはまず「機械語」への翻訳を行い、コンピューターが理解できる形の命令コードに変換する必要があります。
PHPなどのインタプリタ言語はプログラムが実行されるたびに「インタプリタ」と呼ばれるソフトがソースコードの解析を行い、処理をひとつひとつ解釈しながら実行する仕組みになっています。このためソースコードを予め機械語への翻訳する必要がない反面、毎回ソースコードの解析などプログラムの内容自体とは直接関係のない処理が必要になるため実行速度はどうしても遅くなってしまいます。
(インタプリタ言語の仕組み)
反対にGoなどのコンパイラ言語は、プログラムのソースコードをあらかじめ「コンパイラ」と呼ばれるソフトを使って機械語に変換(コンパイル)しておき、できあがったファイルを直接実行するようになっています。実行時にはこうしてできあがった機械語の実行コードを直接実行するため、無駄がなくとても早くコードを実行できるのです。
また近年はコンパイル時にプログラムの処理を自動的に最適化してくれる実装が一般的となっていて、無駄を省いたり冗長な処理を一度に実行したりするなど、プログラムはさらに高速に実行されるようになっています。
(コンパイラ言語の仕組み)
さて、このコンパイル言語という特徴を活かしてGo言語ではPHPにはないメリットを享受することができます。
Go vs PHP: バグの見つけやすさ
プログラマーが常に悩まされるのがプログラムの不具合、「バグ」の存在です。ひとたびバグが起これば意図した処理が行われず、エラー画面を出して処理が止まったり、酷いものだと誤った結果を出してしまったり不正な情報を記録したりしてしまいます。
これを防ぐため、プログラムの完成後はプログラマーや「デバッガー」と呼ばれるテスト専用のスタッフによって、入念にテストを重ねバグを見つけ出したり、見つかったバグをひたすら修正する「デバッグ」という作業に没頭することになります。規模の大きなプログラムになるとこの手間も膨大になり、そのコストはとても無視できなくなってきます。この対策として主に「記述ミスを機械的に見つけ出して、バグの発生を未然に防ぐ」「細かい単位のテストは自動化して、人間がテストする手間を減らす」というものが挙げられます。
PHPとGo言語を比較したときの大きなアドバンテージは、このテストの行いやすさが挙げられます。Goは前述のとおりプログラムの完成後にコンパイルを行うのですが、このときにいくつかのエラーが検出され、問題があればコンパイルできないようになっています。また「go vet
」というコマンドが用意されていて、コンパイル時に検出できないロジック上の問題もある程度検出できるようになっています。
構文エラーの検出
まずプログラム自体の記法を間違えたときに起きる「構文エラー」。単純な例だと波括弧の数が一致していなかったり、コロン (:) とセミコロン (;) を間違えて打ってしまった場合などに発生します。
PHPではプログラムを実行してファイルが読み込まれたタイミングで解析が走るため、事前にチェックしておかないと実際にそのエラーのあるファイルが実行されるまで気づくことができず、構文エラーさえも見過ごしてしまいかねません。
もちろんこれをチェックする「リント」というコマンド (php -l
) はあるのですが、ファイル一つ一つを検証しなければならずその自動化も手間がかかるため、導入していないケースも多いでしょう。
この点Goではコンパイルした時点で必要なファイルが全て読み込まれて解析されるため、構文エラーが一箇所でもあればすぐにエラーが検出されるのです。このおかげでずっと構文エラーを放置し続けてしまうことを防ぐことができます。
(Go言語では文法エラーがコンパイル時に検出される)
参照エラーの検出
またPHPの構文チェックではたとえば「存在しない変数の参照」や「参照できない関数の呼び出し」などは検出できません。これらは構文上は問題ないため、そのコードが実際に実行されるまで機械的に検出することができないのです。
(PHPにおける存在しない関数の呼び出し ― 実際に呼び出すコードが実行されるまでエラーが出ない)
しかしGoではこれらもコンパイル時にチェックされ、エラーが出力されるのです。コンパイラ言語のメリットはこのあたりにも生きてきます。
(Go言語における存在しない変数の呼び出し ― コンパイルによってエラーが検出される)
加えてGoでは他のコンパイラ言語にはない特徴として、「不具合が起きそうな記述は許さない」という設計思想になっています。
具体例として、Go言語では使われていない変数や「値を代入しているが、そのあと読み出されていない」変数、読み込んだが使っていない外部ライブラリなどがあった場合はコンパイル時にエラーがでるようになっています。
この理由として「未使用の変数はバグの可能性がある」「不要なimportはコンパイルを遅くさせる」というものがあり、この制限のおかげでうっかりしたミスや書き忘れ、書き間違えによるエラーを防げるようになっています。
ロジックエラーの対策
上記の構文エラーはバグのなかでは軽微なもので、PHPであってもGoであっても問題があれば最終的にプログラムはエラーを出してくれるのでバグの存在とその箇所を特定することは簡単にできます。
しかしGo言語ではさらに1歩進んだエラーチェックの機能が用意されていて、上述した「go vet
」コマンドがそれに当たります。
VetコマンドはGoのソースコードを検証して、怪しげな構造を検出してくれます。具体的にはデータを文字列として整形する「Printf
」のフォーマット構文がおかしかったり、プログラム上絶対にたどり着かないコード、構造体に付けられる「タグ」の間違った書き方などを教えてくれるのです。
(vetにより検出されたロジック上の問題点 ― 途中でreturnされているため最後のコードは絶対に実行されない)
他にも実際にvetに助けられる例としてこちらで紹介されているような「ループ変数を並列処理関数に渡してしまって、想定通りの結果が得られなくなっている」ケースなど、vetはいかにも「ありがちな」ミスなどを見つけ出してくれるとても有用なツールとなっています。
ユニットテストによるテストの自動化
しかしバグの中には「構文としては正しいが、意図した動作をしない」ものもあります。非常に簡単な例だと「2足す3」を計算するつもりの式を「2×3
」と書いてしまうようなケースが挙げられます。これは機械的には正しく「書かれたとおりに」動くため、自動的に検出する手立てはありません。
こういったバグになるとテストを入念に行うしかもはや見つけ出す方法はなくなるのですが、複雑なプログラムのあらゆる動作をすべて毎回人間がチェックするのはあまりにもコストがかかります。このため近年では機能ごとに「ユニットテスト」と呼ばれる単体テスト用のプログラムを別途に書いて、機能ごとに意図した計算結果が返ってくるか機械的にテストする手法が採られるようになってきています。
このユニットテストの考え方はもちろんPHPにも輸入され、広く使われているものだと「PHPUnit」というフレームワークを使ってテストコードを書くことが主流になっています。しかし言語に標準でユニットテストの機能が組み込まれているわけではなく、PHPUnitの導入も多少なりとも手間がかかってしまいます。
この点Go言語では一歩進んだアプローチが採られていて、言語自体に標準でユニットテストを行う機能が含まれているのです。この手順も非常に簡単で、テストを行いたいファイルと同じ場所に「_test.go
」という名前で終わるテストファイルを置き、「go test
」というコマンドを実行するだけでテストを行えようになっています。
(Go言語におけるユニットテスト)
こういった数々の特徴により、Go言語ではPHPよりもずっとバグの残りにくい「安全な」コードを書きやすくなっているのです。
Go vs PHP: 厳格なGoと、ゆるいPHP
あらゆるプログラムは、変数に「型」という情報を持っています。型というのは変数のデータが「数値」なのか「文字列」なのかなどといった情報で、PHPでも内部的に型を持っています。
$integer = 32; // 整数
$float = 8.25; // 小数
$string = "12.5"; // 文字列
$array = [1, 2, 3]; // 配列
(PHPにおける動的型付けの例)
PHPは「動的型付け」という型システムを採用していて、これはPHPが自動的に変数の型を検知して割り当ててくれるようになっているものです。
このためプログラマーは(少なくとも定義する時点では)変数の型を気にする必要がなく、より気軽にプログラムを書けるようになっています。
PHPではたとえば下図のように、数値型と文字列型の足し算を行ったり、数値型を文字列型に変換して同じ変数に代入したりできます。
echo 12 + "010"; // 22
$i = 10;
$i = "$i";
var_dump($i); // string(2) "10"
(PHPでは暗黙的に型の変換が行われる)
これに対してGoは「静的型付け」を採用していて、変数が必ず特定の型を持ち、他の型に変更できない仕様になっています。
下記の例では変数「i
」を定義して整数 (int) の値を代入し(この時点で自動的に整数として変数i
が定義されます)、次の行でその変数に文字列 (string) を代入しようとしたものです。Goのコンパイラーはこれをエラーとして検出しました。
このように、非常に厳格に型が判断されるようになっています。この特性は一見面倒くさいようにも見えるのですが、このおかげでバグが起こりにくくなるという大きなメリットがあります。
PHPの型変換でよく起きる思わぬ動作の一つに下記のようなものがあります。この例では数値と文字列の比較を行っていて、人間の目にはその内容はまったく異なるのですが、PHPはなんとどちらのケースも真を返しています。
var_dump(0 == "0a"); // bool(true)
var_dump(123 == "123abc"); // bool(true)
この理由は、整数と文字列を比較する際にPHPによって自動的に型変換が行われることによります。
PHPは上記の例で比較されている文字列を「整数として」変換してしまい、アルファベットを除外して先頭の数字だけが数値として変換された結果、2つとも同じ値として認識されてしまうのです。
よくある例として、以下のコードをご覧ください。
$john = User::getByName("John");
if (isset($john)) {
if (empty($john->age)) {
echo "We don't know how old he is.";
} else {
echo "John is {$john->age} year(s) old";
}
}
上記のコードにおけるUser::getByName()
の動作としては、「おそらくユーザーが存在すればオブジェクトが返ってきて、存在しなければnull
が返ってくる」と期待したコードであることが類推できます。
しかしたとえば、実はこの動作が「ユーザーが存在しなければfalse
が返ってくる」という仕様だった場合はどうなるでしょう。このプログラムでは「ユーザーが存在するもの」として処理を進めてしまいます。
またその次にあるempty()
による条件にも問題があり、このコードではJohnが「0歳」だった場合「年齢不詳」と出てしまうのです。
empty()
はnull
やfalse
をまとめて取り扱える反面、数値の0
や空文字列 (""
) なども同様に「空」として判断してしまうのです。
このようにPHPなど動的な型システムを持つ言語では、「型が自由である」かわりにプログラマーが常に取り回される変数の型を潜在的に意識し続ける必要があり、そこでうっかりしたミスが発生してしまっても言語は助けてくれないのです。
PHP7では若干これを改善する機能が追加され、関数の返す型を指定できる機能が追加されました。これによって、実行時に関数が想定と違う型を返した場合はエラーがでるようになっています。
しかしこれはあくまで関数が出力する型がチェックされるだけで中間の処理は相変わらず型が動的に変換されますし、なによりこの評価はあくまで実行時に行われるため、実際にその関数が呼びだされ想定と違う型の値を返すまではバグを検出することができません。
この点静的な型付けを行うGo言語では関数が返す値も含め全ての変数の型は必ず一定であり、たとえ途中で仕様変更が行われて関数の返り値が変更されたとしても、それを参照していたコードは型の不一致でコンパイル時にきちんとエラーがでるようになります。
静的型付けであえて型を厳格にすることによって、特に複数人で進めるような開発や長期的に運用していくようなプロジェクトでは、メンテナンス性の確保にも繋がり安定性の高いプログラムを作りやすくなります。
もちろんこの型付けの厳格さは時としてプログラマーの「めんどくさい」「不自由」という思いにも繋がったりするのですが、バグを防ぎやすくするという意味では非常に有益な特性となっています。
Go vs PHP: パッケージ管理と充実した標準ライブラリ
PHPは標準ライブラリがそこまで贅沢でなく、欲しい機能を探すとどれもPECL拡張モジュールを必要としていたりと手間がかかる事が少なくありません。
また外部ライブラリの導入に際しては近年でこそパッケージ管理システムとしてComposerの利用が一般的になってきましたが、その導入の手間も全くゼロというわけではなく、また言語レベルで柔軟にサポートされているわけではないので、ライブラリを読み込むために(比較的大きなオーバーヘッドである)autoloadが必要だったりします。
この点においてGo言語は優れた特徴があり、まずGoは標準ライブラリがかなり充実していて大抵の機能は標準ライブラリで揃ったりします。
具体的にはPHPで利用する際に原則として外部ライブラリのzlibやmcryptが必要になる圧縮や暗号化の機能、icuが必要なユニコードの処理、GDやImageMagickを必要としていた画像処理、PHP単体では実質的に実現不可能なHTTPサーバー機能など、他にもあらゆる機能が純粋なGoで書かれて標準ライブラリとして提供されています。
また外部ライブラリを利用し管理する際に使うパッケージ管理についても、言語レベルでその機能が備えられています。
Go言語における依存パッケージの読み込みは、GitHubなどのURLをimport構文に書くだけで完結します。あとは「go get
」コマンドを実行するだけで、自動的にimportされているパッケージがダウンロードされるようになっています。
import (
"net/http"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/ec2"
)
(Go言語におけるパッケージの呼び出し)
Go vs PHP: 並列処理
GoとPHPの決定的な違いのひとつに、「並列処理の可否」があります。
複数の処理を同時に走らせてスピードアップを図ったり、延々と作業をする傍らもう一つの処理でその進捗を監視したり、ずっとデータを待ち受けて届いた都度処理を行ったり、さまざまなことができるようになる並列処理。
PHPでは、この並列処理の敷居がわりと高いのです。
PHPはもともと並列処理を行うように作られていないため、純正で並列処理を行う機能がサポートされていません。
後述するいくつかの限られた手法を使って「並列処理っぽいこと」ができなくはないのですが、その敷居も高くとても面倒くさいものとなっています。
まずPHPで並列処理を行うための妥当な選択肢としては近年PECLモジュールにpthreads
というものが登場し、このモジュールを使えばPHPでもマルチスレッドの並列処理を行うことは可能になりました。
ただし残念なことにPECLモジュールということで別途インストールが必要であり、純正で対応できるわけではないためどうしても手軽さは欠けてしまいます。
またpthreadsを使わずpcntl_forkによるプロセスフォークで無理やり実装したり並列cURLを使って別のPHPを遠隔で叩くという大道芸のような解決策もあったりはしますが、いずれも色々問題があるので、PHPで綺麗に並列処理を行うのであれば実質的にpthreads一択になるかと思います。
このような実情もあって、PHPで並列処理を行うことはあまり現実的な方法だとは捉えられてきませんでした。
この点でGo言語は非常に優れた並列処理の機能を持っていて、他の並列処理を行える言語と比較しても簡単で書きやすい便利なものになっています。
並列処理ができれば前述のような高速化をはじめ、たとえばGo単体でWebサーバーやメールサーバーなどを作ったりすることも容易に行なえます。
実際、標準ライブラリのnet/http
を使えば数行で高機能なWebサーバーを立ち上げることも可能です。
Go言語での並列処理の書き方は非常に簡単で、並列処理したい関数を「go」を付けて呼び出すだけで自動的に並列化されるようになります。この機能をGo言語では「ゴルーチン」と呼んでいます。
func main() {
go func(){
// このブロックは独立して並列に処理されます。
}()
// ここに書いたプログラムは上のブロックと並列で実行されます。
}
(ゴルーチンによる並列処理)
またこの非同期的な並列処理を、「チャンネル」という機能を使っていずれかのタイミングで同期させることも可能です。つまり複数の並列処理で行った計算結果をまとめることが可能なのです。
func main() {
// 整数型のチャンネルを作成
c := make(chan int)
go func(){
// チャンネルに計算結果を送信。
c <- 100
}()
// なんらかの別の処理
// チャンネルの送信を待って、値を受け取る
result := <- c
}
(チャンネルを用いたデータの受け渡し)
また並列化の特徴を活かしてGo言語ではサーバーを作ることも可能になっていて、たとえばGoでHTTPサーバーを建てるなら必要最低限下記のコードだけでサーバーを作ることができます。
package main
import "net/http"
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("foo bar"))
})
http.ListenAndServe(":80", nil)
}
たったこれだけで、nginxやapacheなどを別途用意することなくHTTPサーバーを建てられるのです!
次章予告:なぜ弊社はPHPからGo言語に乗り換えたのか
さて、上記のようにGo言語には数多の魅力的な特徴があります。
しかし同時にPHPからGo言語へおいそれと簡単には移行できない理由もありました。その事情とはなんだったのか、どう乗り越えられたのか、Go言語の本格導入によってプロジェクトはどう変わったのでしょうか。
LIGがGo言語を導入した裏話を、次の記事で紹介していきたいと思います。
それでは、乞うご期待!
* License: Icons made by Madebyoliver and Freepik from www.flaticon.com is licensed by CC BY 3.0
LIGはWebサイト制作を支援しています。ご興味のある方は事業ぺージをぜひご覧ください。